diff --git a/.gitignore b/.gitignore index 28395704..9471e231 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* +phel-error.log .DS_Store Thumbs.db diff --git a/content/documentation/arithmetic.md b/content/documentation/arithmetic.md index 8d7cc2f0..f8a8a843 100644 --- a/content/documentation/arithmetic.md +++ b/content/documentation/arithmetic.md @@ -12,6 +12,24 @@ All arithmetic operators are entered in prefix notation. (+ 1 (* 2 2) (/ 10 5) 3 4 (- 5 6)) # Evaluates to 13 ``` +{% php_note() %} +Phel uses prefix notation (operator comes first) instead of PHP's infix notation: + +```php +// PHP - infix notation +1 + (2 * 2) + (10 / 5) + 3 + 4 + (5 - 6); + +// Phel - prefix notation +(+ 1 (* 2 2) (/ 10 5) 3 4 (- 5 6)) +``` + +This allows operators to accept any number of arguments and eliminates operator precedence concerns. +{% end %} + +{% clojure_note() %} +Arithmetic works exactly like Clojure—prefix notation with variadic support for most operators. +{% end %} + Some operators support zero, one or multiple arguments. ```phel @@ -34,6 +52,27 @@ Some operators support zero, one or multiple arguments. (/ 24 4 2) #Evaluates to 3 ``` +{% php_note() %} +Phel's variadic operators are more flexible than PHP's: + +```php +// PHP - requires at least two operands +1 + 2 + 3 + 4 + 5; +// Can't do this: +(); <- syntax error + +// Phel - supports 0, 1, or many operands +(+) # 0 (identity) +(+ 1) # 1 (identity) +(+ 1 2 3 4 5) # 15 (sum of all) +``` + +**Useful patterns:** +- `(+)` returns the additive identity (0) +- `(*)` returns the multiplicative identity (1) +- `(- x)` negates a number +- `(/ x)` computes the reciprocal +{% end %} + Further numeric operations are `%` to compute the remainder of two values and `**` to raise a number to the power. All numeric operations can be found in the API documentation. Some numeric operations can result in an undefined or unrepresentable value. These values are called _Not a Number_ (NaN). Phel represents these values by the constant `NAN`. You can check if a result is NaN by using the `nan?` function. @@ -44,6 +83,24 @@ Some numeric operations can result in an undefined or unrepresentable value. The (nan? NAN) # true ``` +{% php_note() %} +NaN handling is similar to PHP: + +```php +// PHP +is_nan(1); // false +is_nan(log(-1)); // true +is_nan(NAN); // true + +// Phel +(nan? 1) # false +(nan? (php/log -1)) # true +(nan? NAN) # true +``` + +The `%` operator for remainder and `**` for exponentiation work like PHP's `%` and `**` operators. +{% end %} + ## Bitwise Operators Phel allows the evaluation and manipulation of specific bits within an integer. @@ -80,3 +137,31 @@ Phel allows the evaluation and manipulation of specific bits within an integer. (bit-test 0b1011 0) # Evaluates to true (bit-test 0b1011 2) # Evaluates to false ``` + +{% php_note() %} +Phel provides named functions for bitwise operations instead of PHP's operators: + +```php +// PHP bitwise operators +0b1100 & 0b1001; // AND +0b1100 | 0b1001; // OR +0b1100 ^ 0b1001; // XOR +~0b0111; // NOT +0b1101 << 1; // Left shift +0b1101 >> 1; // Right shift + +// Phel named functions +(bit-and 0b1100 0b1001) +(bit-or 0b1100 0b1001) +(bit-xor 0b1100 0b1001) +(bit-not 0b0111) +(bit-shift-left 0b1101 1) +(bit-shift-right 0b1101 1) +``` + +Phel also provides additional bit manipulation functions not available in PHP: `bit-set`, `bit-clear`, `bit-flip`, and `bit-test`. +{% end %} + +{% clojure_note() %} +Bitwise operators work exactly like Clojure's—same function names and behavior for bit manipulation. +{% end %} diff --git a/content/documentation/basic-types.md b/content/documentation/basic-types.md index afd44316..34ec1270 100644 --- a/content/documentation/basic-types.md +++ b/content/documentation/basic-types.md @@ -5,7 +5,7 @@ weight = 2 ## Nil, True, False -Nil, true and false are literal constants. In Phel, `nil` is the same as `null` in PHP. Phel's `true` and `false` are the same as PHP's `true` and `false`. +Nil, true and false are literal constants. ```phel nil @@ -13,6 +13,30 @@ true false ``` +In Phel, only `false` and `nil` are falsy. Everything else is truthy—including `0`, `""`, and `[]`. + +```phel +# Truthiness examples +(if nil "yes" "no") # => "no" (nil is falsy) +(if false "yes" "no") # => "no" (false is falsy) +(if 0 "yes" "no") # => "yes" (0 is truthy!) +(if "" "yes" "no") # => "yes" (empty string is truthy!) +(if [] "yes" "no") # => "yes" (empty vector is truthy!) +``` + +{% php_note() %} +In PHP, `nil` is the same as `null`, and `true`/`false` are the same. However, truthiness works differently: + +**PHP**: `0`, `""`, `[]`, `null`, and `false` are all falsy +**Phel**: Only `false` and `nil` are falsy + +This means `if (0)` in PHP is false, but `(if 0 ...)` in Phel is true! +{% end %} + +{% clojure_note() %} +Truthiness is the same as Clojure—only `false` and `nil` are falsy. +{% end %} + ## Symbol Symbols are used to name functions and variables in Phel. @@ -26,7 +50,7 @@ my-module/my-function ## Keywords -A keyword is like a symbol that begins with a colon character. However, it is used as a constant rather than a name for something. +A keyword is like a symbol that begins with a colon character. However, it is used as a constant rather than a name for something. Keywords are interned and fast for equality checks. ```phel :keyword @@ -36,6 +60,35 @@ A keyword is like a symbol that begins with a colon character. However, it is us :: ``` +Keywords are commonly used as map keys: + +```phel +# Map with keyword keys +{:name "Alice" :email "alice@example.com"} + +# Accessing map values with keywords +(get {:name "Alice" :age 30} :name) # => "Alice" +(:name {:name "Alice" :age 30}) # => "Alice" (keywords are functions!) +``` + +{% php_note() %} +Keywords are like string constants, but more efficient for map keys. Use keywords instead of strings for map keys: + +```phel +# Less idiomatic: +{"name" "Alice" "age" 30} + +# Idiomatic: +{:name "Alice" :age 30} +``` + +Keywords are interned (only one instance exists in memory), making equality checks very fast. +{% end %} + +{% clojure_note() %} +Keywords work exactly like in Clojure—they're interned, fast for equality checks, and self-evaluate. +{% end %} + ## Numbers Phel supports integers and floating-point numbers. Both use the underlying PHP implementation. Integers can be specified in decimal (base 10), hexadecimal (base 16), octal (base 8) and binary (base 2) notations. Binary, octal and hexadecimal formats may contain underscores (`_`) between digits for better readability. @@ -69,9 +122,7 @@ Phel supports integers and floating-point numbers. Both use the underlying PHP i ## Strings -Strings are surrounded by double quotes. They almost work the same as PHP double-quoted strings. One difference is that the dollar sign (`$`) must not be escaped. Internally, Phel strings are represented by PHP strings. Therefore, every PHP string function can be used to operate on the string. - -Strings can be written over multiple lines. The line break character is then ignored by the reader. +Strings are surrounded by double quotes. The dollar sign (`$`) does not need to be escaped. ```phel "hello world" @@ -89,9 +140,28 @@ string." "Hexadecimal notation is supported: \x41" -"Unicodes can be encoded as in PHP: \u{1000}" +"Unicodes can be encoded: \u{1000}" +``` + +String concatenation and conversion using `str`: + +```phel +(str "Hello" " " "World") # => "Hello World" +(str "The answer is " 42) # => "The answer is 42" +``` + +{% php_note() %} +Phel strings are PHP strings internally, so you can use all PHP string functions: + +```phel +(php/strlen "hello") # => 5 +(php/strtoupper "hello") # => "HELLO" +(php/str_replace "o" "0" "hello") # => "hell0" ``` +Strings work almost the same as PHP double-quoted strings, with one difference: the dollar sign (`$`) doesn't need escaping. +{% end %} + ## Lists A list is a sequence of whitespace-separated values surrounded by parentheses. @@ -118,17 +188,42 @@ A vector in Phel is an indexed data structure. In contrast to PHP arrays, Phel v ## Maps -A map is a sequence of whitespace-separated key/value pairs surrounded by curly braces, wherein the key and value of each key/value pair are separated by whitespace. There must be an even number of items between curly braces or the parser will signal a parse error. The sequence is defined as key1, value1, key2, value2, etc. +A map is a sequence of whitespace-separated key/value pairs surrounded by curly braces. The sequence is defined as key1, value1, key2, value2, etc. There must be an even number of items. ```phel {} # same as (hash-map) {:key1 "value1" :key2 "value2"} -{'(1 2 3) '(4 5 6)} -{[] []} -{1 2 3 4 5 6} + +# Any type can be a key +{'(1 2 3) '(4 5 6)} # Lists as keys +{[] []} # Vectors as keys +{1 2 3 4 5 6} # Numbers as keys + +# Common pattern: keywords as keys +{:name "Alice" :age 30 :email "alice@example.com"} +``` + +{% php_note() %} +Unlike PHP associative arrays, Phel maps: +- Can have **any type** as keys (not just strings/integers): vectors, lists, or even other maps +- Are **immutable**: operations return new maps without modifying the original +- Are **not** PHP arrays internally—they're their own data structure + +```phel +# PHP: +$map = ['name' => 'Alice']; +$map['name'] = 'Bob'; // Mutates in place + +# Phel: +(def map {:name "Alice"}) +(def new-map (assoc map :name "Bob")) # Returns new map +# map is still {:name "Alice"} ``` +{% end %} -In contrast to PHP associative arrays, Phel maps can have any types of keys. +{% clojure_note() %} +Maps work exactly like Clojure maps, including support for any hashable type as keys. +{% end %} ## Sets diff --git a/content/documentation/control-flow.md b/content/documentation/control-flow.md index 108cd83a..f61c6b6e 100644 --- a/content/documentation/control-flow.md +++ b/content/documentation/control-flow.md @@ -14,12 +14,33 @@ A control flow structure. First evaluates _test_. If _test_ evaluates to `true`, The _test_ evaluates to `false` if its value is `false` or equal to `nil`. Every other value evaluates to `true`. In sense of PHP this means (`test != null && test !== false`). ```phel +# Basic if examples (if true 10) # Evaluates to 10 (if false 10) # Evaluates to nil (if true (print 1) (print 2)) # Prints 1 but not 2 -(if 0 (print 1) (print 2)) # Prints 2 -(if nil (print 1) (print 2)) # Prints 2 -(if [] (print 1) (print 2)) # Prints 2 + +# Important: Only false and nil are falsy! +(if 0 (print 1) (print 2)) # Prints 1 (0 is truthy!) +(if nil (print 1) (print 2)) # Prints 2 (nil is falsy) +(if [] (print 1) (print 2)) # Prints 1 (empty vector is truthy!) + +# Practical examples +(defn greet [name] + (if name + (str "Hello, " name) + "Hello, stranger")) + +(greet "Alice") # => "Hello, Alice" +(greet nil) # => "Hello, stranger" + +# Using if for validation +(defn divide [a b] + (if (= b 0) + nil + (/ a b))) + +(divide 10 2) # => 5 +(divide 10 0) # => nil ``` ## Case @@ -31,17 +52,71 @@ The _test_ evaluates to `false` if its value is `false` or equal to `nil`. Every Evaluates the _test_ expression. Then iterates over each pair. If the result of the test expression matches the first value of the pair, the second expression of the pair is evaluated and returned. If no match is found, returns nil. ```phel +# Basic case examples (case (+ 7 5) 3 :small 12 :big) # Evaluates to :big (case (+ 7 5) 3 :small - 15 :big) # Evaluates to nil + 15 :big) # Evaluates to nil (no match) + +(case (+ 7 5)) # Evaluates to nil (no pairs) + +# Practical examples +(defn http-status-message [code] + (case code + 200 "OK" + 201 "Created" + 400 "Bad Request" + 404 "Not Found" + 500 "Internal Server Error")) + +(http-status-message 200) # => "OK" +(http-status-message 404) # => "Not Found" +(http-status-message 999) # => nil + +# Using case with keywords +(defn animal-sound [animal] + (case animal + :dog "Woof!" + :cat "Meow!" + :cow "Moo!" + :duck "Quack!")) + +(animal-sound :dog) # => "Woof!" +(animal-sound :fish) # => nil +``` -(case (+ 7 5)) # Evalutes to nil +{% php_note() %} +`case` is similar to PHP's `switch` but more concise: + +```php +// PHP +switch ($value) { + case 3: + $result = 'small'; + break; + case 12: + $result = 'big'; + break; + default: + $result = null; +} + +// Phel +(case value + 3 :small + 12 :big) ``` +No `break` needed—Phel's `case` doesn't fall through. +{% end %} + +{% clojure_note() %} +`case` works exactly like Clojure's `case`—evaluates to the matching value without fall-through. +{% end %} + ## Cond ```phel @@ -51,17 +126,79 @@ Evaluates the _test_ expression. Then iterates over each pair. If the result of Iterates over each pair. If the first expression of the pair evaluates to logical true, the second expression of the pair is evaluated and returned. If no match is found, returns nil. ```phel +# Basic cond examples (cond (neg? 5) :negative (pos? 5) :positive) # Evaluates to :positive (cond (neg? 5) :negative - (neg? 3) :negative) # Evaluates to nil + (neg? 3) :negative) # Evaluates to nil (no match) + +(cond) # Evaluates to nil (no pairs) + +# Practical examples +(defn classify-number [n] + (cond + (< n 0) "negative" + (= n 0) "zero" + (> n 0) "positive")) + +(classify-number -5) # => "negative" +(classify-number 0) # => "zero" +(classify-number 10) # => "positive" + +# Using cond for complex conditions +(defn ticket-price [age] + (cond + (< age 3) 0 # Free for toddlers + (< age 12) 5 # Child price + (< age 65) 10 # Adult price + :else 7)) # Senior discount + +(ticket-price 2) # => 0 +(ticket-price 10) # => 5 +(ticket-price 30) # => 10 +(ticket-price 70) # => 7 + +# Combining multiple conditions +(defn water-state [temp] + (cond + (<= temp 0) :ice + (and (> temp 0) (< temp 100)) :liquid + (>= temp 100) :steam)) + +(water-state -5) # => :ice +(water-state 25) # => :liquid +(water-state 105) # => :steam +``` + +{% php_note() %} +`cond` is like a chain of `if`/`elseif` in PHP: + +```php +// PHP +if ($value < 0) { + $result = 'negative'; +} elseif ($value > 0) { + $result = 'positive'; +} else { + $result = null; +} -(cond) # Evaluates to nil +// Phel +(cond + (neg? value) :negative + (pos? value) :positive) ``` +More elegant for multiple conditions than nested `if` expressions. Use `:else` as the last condition for a default case. +{% end %} + +{% clojure_note() %} +`cond` works exactly like Clojure's `cond`—evaluates predicates in order and returns first match. +{% end %} + ## Loop ```phel @@ -77,18 +214,71 @@ Evaluates the expressions in order and rebinds them to the recursion point. A re Internally `recur` is implemented as a PHP while loop and therefore prevents the _Maximum function nesting level_ errors. ```phel +# Basic loop example - sum numbers from 1 to 10 (loop [sum 0 cnt 10] (if (= cnt 0) sum - (recur (+ cnt sum) (dec cnt)))) + (recur (+ cnt sum) (dec cnt)))) # => 55 + +# Recursion in a function +(defn factorial [n] + (loop [acc 1 + n n] + (if (<= n 1) + acc + (recur (* acc n) (dec n))))) + +(factorial 5) # => 120 + +# Finding an element in a vector +(defn find-index [pred coll] + (loop [idx 0 + items coll] + (cond + (empty? items) nil + (pred (first items)) idx + :else (recur (inc idx) (rest items))))) + +(find-index even? [1 3 5 8 9]) # => 3 +(find-index neg? [1 2 3]) # => nil + +# Building a result with loop +(defn reverse-vec [v] + (loop [result [] + remaining v] + (if (empty? remaining) + result + (recur (conj result (last remaining)) + (pop remaining))))) + +(reverse-vec [1 2 3 4]) # => [4 3 2 1] +``` -(fn [sum cnt] - (if (= cnt 0) - sum - (recur (+ cnt sum) (dec cnt)))) +{% php_note() %} +`loop`/`recur` provides tail-call optimization, which PHP doesn't support natively: + +```php +// PHP - recursive functions can cause stack overflow +function countdown($n) { + if ($n === 0) return 0; + return countdown($n - 1); // Stack overflow for large n! +} + +// Phel - recur compiles to a while loop (safe for any n) +(loop [n 1000000] + (if (= n 0) + 0 + (recur (dec n)))) # No stack overflow! ``` +This is critical for functional programming patterns in PHP. +{% end %} + +{% clojure_note() %} +`loop`/`recur` works exactly like Clojure—provides tail-call optimization by compiling to iterative loops. +{% end %} + ## Foreach ```phel @@ -106,6 +296,32 @@ The `foreach` special form can be used to iterate over all kind of PHP datastruc (print v)) # Prints "a", 1, "b" and 2 ``` +{% php_note() %} +`foreach` mirrors PHP's foreach loop syntax: + +```php +// PHP +foreach ([1, 2, 3] as $v) { + print($v); +} + +foreach (["a" => 1, "b" => 2] as $k => $v) { + print($k); + print($v); +} + +// Phel +(foreach [v [1 2 3]] + (print v)) + +(foreach [k v {"a" 1 "b" 2}] + (print k) + (print v)) +``` + +**Note:** Prefer `for` or `loop` when you need to return values. `foreach` is only for side-effects. +{% end %} + ## For A more powerful loop functionality is provided by the `for` loop. The `for` loop is an elegant way to define and create arrays based on existing collections. It combines the functionality of `foreach`, `let`, `if` and `reduce` in one call. @@ -145,11 +361,11 @@ have the form `:modifier argument`. The following modifiers are supported: (for [[k v] :pairs {:a 1 :b 2 :c 3}] [v k]) # Evaluates to [[1 :a] [2 :b] [3 :c]] (for [[k v] :pairs [1 2 3]] [k v]) # Evaluates to [[0 1] [1 2] [2 3]] (for [[k v] :pairs {:a 1 :b 2 :c 3} :reduce [m {}]] - (put m k (inc v))) # Evaluates to {:a 2 :b 3 :c 4} + (assoc m k (inc v))) # Evaluates to {:a 2 :b 3 :c 4} (for [[k v] :pairs {:a 1 :b 2 :c 3} :reduce [m {}] :let [x (inc v)]] - (put m k x)) # Evaluates to {:a 2 :b 3 :c 4} + (assoc m k x)) # Evaluates to {:a 2 :b 3 :c 4} (for [[k v] :pairs {:a 1 :b 2 :c 3} :when (contains-value? [:a :c] k) :reduce [acc {}]] - (put acc k v)) # Evaluates to {:a 1 :c 3} + (assoc acc k v)) # Evaluates to {:a 1 :c 3} (for [x :in [2 2 2 3 3 4 5 6 6] :while (even? x)] x) # Evaluates to [2 2 2] (for [x :in [2 2 2 3 3 4 5 6 6] :when (even? x)] x) # Evaluates to [2 2 2 4 6 6] @@ -159,6 +375,27 @@ have the form `:modifier argument`. The following modifiers are supported: (for [x :range [0 4] y :range [0 x]] [x y]) # Evaluates to [[1 0] [2 0] [2 1] [3 0] [3 1] [3 2]] ``` +{% php_note() %} +Phel's `for` is a powerful list comprehension, not like PHP's `for` loop: + +```php +// PHP - manual array building +$result = []; +foreach (range(1, 3) as $x) { + $result[] = $x + 1; +} + +// Phel - declarative comprehension +(for [x :in [1 2 3]] (inc x)) # [2 3 4] +``` + +Phel's `for` combines iteration, filtering (`:when`), early termination (`:while`), reduction (`:reduce`), and nested loops in one elegant expression—much more powerful than PHP's `for`/`foreach`. +{% end %} + +{% clojure_note() %} +`for` works similarly to Clojure's `for`—list comprehensions with `:let`, `:when`, and nested bindings. The `:reduce` modifier is a Phel extension. +{% end %} + # Do ```phel diff --git a/content/documentation/data-structures.md b/content/documentation/data-structures.md index 072df936..5799a8f1 100644 --- a/content/documentation/data-structures.md +++ b/content/documentation/data-structures.md @@ -3,7 +3,17 @@ title = "Data structures" weight = 9 +++ -Phel has four main data structures. All data structures are persistent data structures. A persistent data structure preserves the previous version of itself when it is modified. Such data structures are also called immutable data structures. The difference of persistent data structures and immutable data structures is that immutable data structures copy the whole data structure, while persistent data structures share unmodified values with their previous versions. +Phel has four main data structures: **Lists**, **Vectors**, **Maps**, and **Sets**. + +All data structures are **persistent** (immutable). A persistent data structure preserves the previous version of itself when it is modified. Unlike naive immutable structures that copy everything, persistent data structures efficiently share unmodified values with their previous versions. When you "modify" a collection, you get a new version while the original remains unchanged. + +{% php_note() %} +Think of this as "copy-on-write" for collections, similar to how PHP's copy-on-write works for variables. This prevents bugs from unexpected mutations—a common issue in PHP where passing arrays to functions can lead to surprising behavior. +{% end %} + +{% clojure_note() %} +Phel's data structures work exactly like Clojure's—they're built on the same persistent data structure algorithms (Bagwell's Hash Array Mapped Tries and similar structures). +{% end %} ## Lists @@ -48,7 +58,7 @@ To get the length of the list the `count` function can be used Vectors are an indexed, sequential data structure. They offer efficient random access (by index) and are very efficient in appending values at the end. -To create a vector wrap the white space seperated values with brackets or use the `vector` function. +To create a vector, wrap the white space separated values with brackets or use the `vector` function. ```phel [1 2 3] # Creates a new vector with three values @@ -64,17 +74,17 @@ To get a value by its index use the `get` function. Similar to list you can use (peek [1 2 3]) # Evaluates to 3 ``` -New values can be appended by using the `push` function. +New values can be appended by using the `conj` function. ```phel -(push [1 2 3] 4) # Evaluates to [1 2 3 4] +(conj [1 2 3] 4) # Evaluates to [1 2 3 4] ``` -To change an existing value use the `put` function +To change an existing value use the `assoc` function ```phel -(put [1 2 3] 0 4) # Evaluates to [4 2 3] -(put [1 2 3] 3 4) # Evaluates to [1 2 3 4] +(assoc [1 2 3] 0 4) # Evaluates to [4 2 3] +(assoc [1 2 3] 3 4) # Evaluates to [1 2 3 4] ``` A vector can be counted using the `count` function. @@ -84,15 +94,26 @@ A vector can be counted using the `count` function. (count [1 2 3]) # Evaluates to 3 ``` +{% php_note() %} +Vectors are like PHP's indexed arrays (`[0 => 'a', 1 => 'b']`), but immutable. Use vectors when you need indexed access. +{% end %} + +{% clojure_note() %} +Vectors work exactly like Clojure vectors—use them for indexed collections and when you need to append efficiently to the end. +{% end %} + ## Maps -A Map contains key-value-pairs in random order. Each possible key appears at most once in the collection. In contrast to PHP's associative arrays, Phel Maps can have any type of keys that implement the `HashableInterface` and the `EqualsInterface`. +A Map contains key-value-pairs in random order. Each possible key appears at most once in the collection. Any type that implements the `HashableInterface` and `EqualsInterface` can be used as a key—including vectors, lists, or even other maps. -To create a map wrap the key and values in curly brackets or use the `hash-map` function. +To create a map, wrap the key and values in curly brackets or use the `hash-map` function. ```phel -{:key1 value1 :key2 value2} # A new has-map using shortcut syntax -(hash-map :key1 value1 :key2 value2) # A new has-map using the function +{:key1 value1 :key2 value2} # A new hash-map using shortcut syntax +(hash-map :key1 value1 :key2 value2) # A new hash-map using the function + +# Any type can be a key +{[1 2] "vector-key" :keyword "keyword-key" "string" "string-key"} ``` Use the `get` function to access a value by its key @@ -103,17 +124,17 @@ Use the `get` function to access a value by its key (get {:a 1 :b 2} :c) # Evaluates to nil ``` -To add or update a key-value pair in the map use the `put` function +To add or update a key-value pair in the map use the `assoc` function ```phel -(put {} :a "hello") # Evaluates to {:a "hello"} -(put {:a "foo"} :a "bar") # Evaluates to {:a "bar"} +(assoc {} :a "hello") # Evaluates to {:a "hello"} +(assoc {:a "foo"} :a "bar") # Evaluates to {:a "bar"} ``` -A value in a map can be removed with the `unset` function +A value in a map can be removed with the `dissoc` function ```phel -(unset {:a "foo"} :a) # Evaluates to {} +(dissoc {:a "foo"} :a) # Evaluates to {} ``` As in the other data structures, the `count` function can be used to count the key-value-pairs. @@ -123,6 +144,167 @@ As in the other data structures, the `count` function can be used to count the k (count {:a "foo"}) # Evaluates to 1 ``` +{% php_note() %} +Maps are like PHP's associative arrays, but with two key differences: + +1. **Any type can be a key** (not just strings/integers): vectors, lists, or even other maps +2. **Immutable**: "updating" a map returns a new map; the original is unchanged + +```phel +# PHP: $arr = ['name' => 'Alice', 'age' => 30]; +# Phel: +{:name "Alice" :age 30} + +# PHP: $arr['age'] = 31; +# Phel: +(assoc {:name "Alice" :age 30} :age 31) +# => {:name "Alice" :age 31} +# Original map is unchanged! +``` +{% end %} + +{% clojure_note() %} +Maps work exactly like Clojure maps—use keywords for keys in most cases. +{% end %} + +## Working with Collections + +Phel provides several core functions for manipulating collections. These functions work across different data structure types. + +### Adding Elements with `conj` + +The `conj` function adds elements to collections. The behavior depends on the collection type to maintain efficiency: + +```phel +# Vectors - appends to end +(conj [1 2 3] 4) # Evaluates to [1 2 3 4] +(conj [] 1 2 3) # Evaluates to [1 2 3] + +# Sets - adds element +(conj #{1 2 3} 4) # Evaluates to #{1 2 3 4} +(conj #{1 2 3} 2) # Evaluates to #{1 2 3} (already present) + +# Lists - prepends to front (for efficiency) +(conj (list 1 2 3) 0) # Evaluates to (0 1 2 3) + +# Maps - adds key-value pair +(conj {:a 1} [:b 2]) # Evaluates to {:a 1 :b 2} +(conj {} [:a 1] [:b 2]) # Evaluates to {:a 1 :b 2} +``` + +### Associating Values with `assoc` + +The `assoc` function associates a value with a key in associative data structures (maps, vectors by index, structs). + +```phel +# Maps - set or update key-value pairs +(assoc {} :a "hello") # Evaluates to {:a "hello"} +(assoc {:a "foo"} :a "bar") # Evaluates to {:a "bar"} +(assoc {:a 1} :b 2 :c 3) # Evaluates to {:a 1 :b 2 :c 3} + +# Vectors - set value at index (can extend by one position) +(assoc [1 2 3] 0 4) # Evaluates to [4 2 3] +(assoc [1 2 3] 3 4) # Evaluates to [1 2 3 4] +(assoc [] 0 "first") # Evaluates to ["first"] +``` + +### Removing Values with `dissoc` + +The `dissoc` function removes a key from a data structure, returning the structure without that key. + +```phel +# Maps - remove key-value pair +(dissoc {:a 1 :b 2} :a) # Evaluates to {:b 2} +(dissoc {:a 1 :b 2 :c 3} :a :c) # Evaluates to {:b 2} + +# Sets - remove element +(dissoc #{1 2 3} 2) # Evaluates to #{1 3} +(dissoc #{1 2 3} 2 3) # Evaluates to #{1} +``` + +### Nested Operations + +For working with nested data structures, Phel provides `-in` variants: + +```phel +# get-in - Access nested values +(get-in {:a {:b {:c 1}}} [:a :b :c]) # Evaluates to 1 +(get-in {:users [{:name "Alice"}]} [:users 0 :name]) # Evaluates to "Alice" + +# assoc-in - Set nested values +(assoc-in {} [:a :b :c] 1) # Evaluates to {:a {:b {:c 1}}} +(assoc-in {:a {:b 1}} [:a :c] 2) # Evaluates to {:a {:b 1 :c 2}} + +# update - Update a value by applying a function +(update {:a 1} :a inc) # Evaluates to {:a 2} +(update [1 2 3] 0 + 10) # Evaluates to [11 2 3] + +# update-in - Update nested values +(update-in {:a {:b 1}} [:a :b] inc) # Evaluates to {:a {:b 2}} +``` + +{% php_note() %} + +### Understanding Immutability vs PHP's Mutability + +```phel +# PHP: Mutable operations +$users = ['Alice', 'Bob']; +$users[] = 'Charlie'; # $users is now ['Alice', 'Bob', 'Charlie'] +echo $users[0]; # Still 'Alice' + +# Phel: Immutable operations +(def users ["Alice" "Bob"]) +(def updated-users (conj users "Charlie")) # New collection +# users is still ["Alice" "Bob"] +# updated-users is ["Alice" "Bob" "Charlie"] + +# PHP: Mutating a map +$config = ['theme' => 'dark', 'lang' => 'en']; +$config['theme'] = 'light'; # Overwrites in place + +# Phel: Creating a new map +(def config {:theme "dark" :lang "en"}) +(def new-config (assoc config :theme "light")) +# config is still {:theme "dark" :lang "en"} +# new-config is {:theme "light" :lang "en"} +``` + +**Why immutability matters:** +- **Thread-safe**: Multiple threads can safely read the same data +- **Predictable**: Functions can't unexpectedly modify your data +- **Time-travel**: Keep old versions for undo/history features +- **Easier debugging**: Data doesn't change "magically" + +**When working with PHP code:** Use `php/aset` for PHP arrays that must be mutable: +```phel +(def php-arr (php/array)) +(php/aset php-arr "key" "value") # Mutates the PHP array +``` + +{% end %} + +{% clojure_note() %} + +### Clojure Compatibility + +Phel follows Clojure's naming conventions exactly: + +| Function | Behavior | Clojure Compatible? | +|----------|----------|---------------------| +| `conj` | Add element (type-specific) | ✓ Yes | +| `assoc` | Associate key with value | ✓ Yes | +| `dissoc` | Dissociate key | ✓ Yes | +| `get` | Get value by key | ✓ Yes | +| `get-in` | Get nested value | ✓ Yes | +| `assoc-in` | Set nested value | ✓ Yes | +| `update` | Update with function | ✓ Yes | +| `update-in` | Update nested with function | ✓ Yes | + +**Migration note:** The older `push`, `put`, and `unset` functions are deprecated since v0.25.0. Use `conj`, `assoc`, and `dissoc` instead for Clojure compatibility. + +{% end %} + ## Structs A Struct is a special kind of Map. It only supports a predefined number of keys and is associated with a global name. The Struct not only defines itself but also a predicate function. @@ -132,7 +314,7 @@ A Struct is a special kind of Map. It only supports a predefined number of keys (let [x (my-struct 1 2 3)] # Create a new struct (my-struct? x) # Evaluates to true (get x :a) # Evaluates to 1 - (put x :a 12)) # Evaluates to (my-struct 12 2 3) + (assoc x :a 12)) # Evaluates to (my-struct 12 2 3) ``` Internally, Phel Structs are PHP classes where each key correspondence to an object property. Therefore, Structs can be faster than Maps. @@ -148,17 +330,17 @@ A new set can be created by using the `set` function or shortcut syntax `#{}` (set 1 2 3) # A new set using the function ``` -The `push` function can be used to add a new value to the Set. +The `conj` function can be used to add a new value to the Set. ```phel -(push #{1 2 3} 4) # Evaluates to #{1 2 3 4} -(push #{1 2 3} 2) # Evaluates to #{1 2 3} +(conj #{1 2 3} 4) # Evaluates to #{1 2 3 4} +(conj #{1 2 3} 2) # Evaluates to #{1 2 3} ``` -Similar to the Map the `unset` function can be used to remove a value from the list +Similar to the Map the `dissoc` function can be used to remove a value from the list ```phel -(unset #{1 2 3} 2) # Evaluates to #{1 3} +(dissoc #{1 2 3} 2) # Evaluates to #{1 3} ``` Again the `count` function can be used to count the elements in the set @@ -212,17 +394,121 @@ For example, if we want to convert a PHP Array to a persistent map. This functio [arr] (let [res (transient {})] # Convert a persistent data to a transient (foreach [k v arr] - (put res k v)) # Fill the transient map (mutable) + (assoc res k v)) # Fill the transient map (mutable) (persistent res))) # Convert the transient map to a persistent map. ``` ## Data structures are functions -In Phel all data structures can also be used as functions. +In Phel all data structures can also be used as functions. This enables concise, elegant code: ```phel ((list 1 2 3) 0) # Same as (get (list 1 2 3) 0) ([1 2 3] 0) # Same as (get [1 2 3] 0) ({:a 1 :b 2} :a) # Same as (get {:a 1 :b 2} :a) (#{1 2 3} 1) # Same as (get #{1 2 3} 1) + +# Practical use with map +(def users [{:name "Alice" :age 30} + {:name "Bob" :age 25}]) +(map :name users) # Evaluates to ["Alice" "Bob"] +``` + +## Practical Example: Working with User Data + +Here's a real-world example combining multiple concepts: + +```phel +# Start with user data +(def user {:id 1 + :name "Alice" + :email "alice@example.com" + :settings {:theme "dark" :notifications true}}) + +# Access nested data +(get-in user [:settings :theme]) # => "dark" + +# Update nested settings immutably +(def updated-user + (assoc-in user [:settings :theme] "light")) +# user still has "dark", updated-user has "light" + +# Add a new field +(def user-with-role + (assoc updated-user :role "admin")) + +# Update using a function +(def user-with-incremented-id + (update user-with-role :id inc)) + +# Working with collections of users +(def users + [{:name "Alice" :active true} + {:name "Bob" :active false} + {:name "Charlie" :active true}]) + +# Filter active users and get their names +(->> users + (filter :active) # Keep only active users + (map :name) # Extract names + (into #{})) # Convert to a set +# => #{"Alice" "Charlie"} + +# Build a map from a PHP array (common when interoping with PHP) +(defn php-response-to-map + "Convert a PHP API response to Phel data structures" + [php-arr] + (let [data (transient {})] + (foreach [k v php-arr] + (assoc data (keyword k) v)) + (persistent data))) + +# Use with nested structures +(def api-response + (php/array "user_id" 123 + "user_name" "Alice" + "is_active" true)) + +(php-response-to-map api-response) +# => {:user_id 123 :user_name "Alice" :is_active true} +``` + +### Common Patterns + +**Building data incrementally:** +```phel +# PHP way (mutable) +# $result = []; +# $result['id'] = 1; +# $result['name'] = 'Alice'; +# return $result; + +# Phel way (immutable) +(-> {} + (assoc :id 1) + (assoc :name "Alice")) +# Or all at once: +{:id 1 :name "Alice"} +``` + +**Updating deeply nested data:** +```phel +(def app-state + {:ui {:sidebar {:width 200 :visible true}} + :user {:name "Alice"}}) + +# Change sidebar visibility +(assoc-in app-state [:ui :sidebar :visible] false) + +# Increment sidebar width +(update-in app-state [:ui :sidebar :width] + 50) +``` + +**Merging data:** +```phel +(def defaults {:theme "light" :lang "en" :debug false}) +(def user-prefs {:theme "dark"}) + +(merge defaults user-prefs) +# => {:theme "dark" :lang "en" :debug false} ``` diff --git a/content/documentation/destructuring.md b/content/documentation/destructuring.md index 9a4cb99d..b248981f 100644 --- a/content/documentation/destructuring.md +++ b/content/documentation/destructuring.md @@ -3,8 +3,9 @@ title = "Destructuring" weight = 10 +++ -Destructuring is a way to bind names to values inside a data structure. -Destructuring works for function parameters, `let` and `loop` bindings. +Destructuring is a way to bind names to values inside a data structure. It provides a concise syntax for extracting values from collections. + +Destructuring works in function parameters, `let` bindings, and `loop` bindings. ### Sequential data structures @@ -24,6 +25,27 @@ Sequential data structures can be extracted using the vector syntax. (apply + a b rest)) # Evaluates to 10 ``` +{% php_note() %} +Destructuring is more powerful than PHP's list() or array unpacking: + +```php +// PHP - limited destructuring +[$a, $b] = [1, 2]; +['a' => $x, 'b' => $y] = ['a' => 1, 'b' => 2]; + +// Phel - full destructuring with nesting and rest +(let [[a [b c] & rest] [1 [2 3] 4 5 6]] + ; a = 1, b = 2, c = 3, rest = (4 5 6) + ) +``` + +Phel's destructuring works in more places (function params, let, loop) and supports more patterns. +{% end %} + +{% clojure_note() %} +Destructuring works exactly like Clojure's destructuring—same syntax and behavior. +{% end %} + ### Associative data structures Associative data structures can be extracted using the map syntax. @@ -36,6 +58,22 @@ Associative data structures can be extracted using the map syntax. (+ a b c)) # Evaluates to 6 ``` +{% php_note() %} +Associative destructuring lets you extract values by key: + +```php +// PHP - manual extraction +$data = ['a' => 1, 'b' => 2]; +$a = $data['a']; +$b = $data['b']; + +// Phel - destructuring +(let [{:a a :b b} {:a 1 :b 2}] + ; a = 1, b = 2 + ) +``` +{% end %} + ### Indexed sequential Indexed sequential can also be extracted by indices using the map syntax. diff --git a/content/documentation/functions-and-recursion.md b/content/documentation/functions-and-recursion.md index c39c9a24..6e3473ad 100644 --- a/content/documentation/functions-and-recursion.md +++ b/content/documentation/functions-and-recursion.md @@ -52,6 +52,24 @@ There is a shorter form to define an anonymous function. This omits the paramete |(sum $&) # Same as (fn [& xs] (sum xs)) ``` +{% php_note() %} +The short-form anonymous function syntax `|` is similar to PHP's arrow functions: + +```php +// PHP +$add = fn($x) => $x + 6; +array_map(fn($x) => $x * 2, $array); + +// Phel +(def add |(+ $ 6)) +(map |(* $ 2) array) +``` +{% end %} + +{% clojure_note() %} +The short-form `|` syntax is inspired by Clojure's `#()` reader macro, but uses different parameter names (`$` instead of `%`). +{% end %} + ## Global functions @@ -107,20 +125,82 @@ Both approaches are equivalent, but `defn-` provides a more concise syntax for d ## Recursion -Similar to `loop`, functions can be made recursive using `recur`. +Similar to `loop`, functions can be made recursive using `recur`. The `recur` special form enables tail-call optimization, preventing stack overflow errors. ```phel -(fn [x] - (if (php/== x 0) - x - (recur (php/- x 1)))) - -(defn my-recur-fn [x] - (if (php/== x 0) - x - (recur (php/- x 1)))) +# Recursive factorial (regular recursion - can stack overflow) +(defn factorial [n] + (if (<= n 1) + 1 + (* n (factorial (dec n))))) + +(factorial 5) # => 120 + +# Tail-recursive factorial using recur with loop +(defn factorial-recur [n] + (loop [acc 1 + n n] + (if (<= n 1) + acc + (recur (* acc n) (dec n))))) + +(factorial-recur 5) # => 120 + +# Recursive sum (can stack overflow on large collections) +(defn sum-recursive [coll] + (if (empty? coll) + 0 + (+ (first coll) (sum-recursive (rest coll))))) + +(sum-recursive [1 2 3 4 5]) # => 15 + +# Tail-recursive sum using recur (safe for large collections) +(defn sum-recur [coll] + (loop [acc 0 + remaining coll] + (if (empty? remaining) + acc + (recur (+ acc (first remaining)) (rest remaining))))) + +(sum-recur [1 2 3 4 5]) # => 15 + +# Using recur directly in function (also tail-call optimized) +(defn countdown [n] + (if (<= n 0) + "Done!" + (do + (println n) + (recur (dec n))))) + +# (countdown 5) # Prints: 5, 4, 3, 2, 1, then returns "Done!" +``` + +{% php_note() %} +`recur` is compiled to a PHP `while` loop, preventing "Maximum function nesting level" errors that would occur with regular recursive calls in PHP. + +```php +// PHP - This will cause stack overflow for large n +function factorial($n) { + if ($n <= 1) return 1; + return $n * factorial($n - 1); // Stack overflow for large n! +} + +// Phel with recur - This works for any size n +(defn factorial-recur [n] + (loop [acc 1 + n n] + (if (<= n 1) + acc + (recur (* acc n) (dec n))))) ``` +**Key difference:** Regular recursion builds up a call stack, while `recur` reuses the same stack frame (tail-call optimization). +{% end %} + +{% clojure_note() %} +`recur` works exactly like Clojure's `recur`—it provides tail-call optimization by compiling to a loop. +{% end %} + ## Apply functions ```phel @@ -144,3 +224,20 @@ Sometimes it is required that a variable should pass to a function by reference. ``` Support for references is very limited in Phel. Currently, it only works for function arguments (except destructuring). + +{% php_note() %} +This is equivalent to PHP's `&` reference operator: + +```php +// PHP +function addToArray(&$arr) { + $arr[] = 10; +} + +// Phel +(defn add-to-array [^:reference arr] + (php/apush arr 10)) +``` + +**Note:** Use references sparingly. Phel's immutable data structures are usually a better choice than mutating PHP arrays. +{% end %} diff --git a/content/documentation/global-and-local-bindings.md b/content/documentation/global-and-local-bindings.md index 8496a901..6c63a41d 100644 --- a/content/documentation/global-and-local-bindings.md +++ b/content/documentation/global-and-local-bindings.md @@ -38,44 +38,128 @@ Creates a new lexical context with assignments defined in bindings. Afterwards t (let [x 1 y (+ x 2)]) # Evaluates to nil ``` + All assignments defined in _bindings_ are immutable and cannot be changed. +{% php_note() %} +`let` creates block-scoped bindings, similar to PHP's block scope, but with immutability: + +```php +// PHP - mutable variables +$x = 1; +$y = $x + 2; +$x = 10; // Can reassign + +// Phel - immutable bindings +(let [x 1 + y (+ x 2)] + ; x = 10 <- This would be a compile error! + (+ x y)) +``` +{% end %} + +{% clojure_note() %} +`let` works exactly like Clojure's `let`—creates lexically scoped, immutable bindings. +{% end %} + ## Binding While `let` creates a new lexical context, `binding` temporarily redefines existing definitions while executing the body. This can be useful when writing tests on functions depending on external state as `binding` allows to remap existing functions or values with mocks. +**Key difference:** +- `let` creates new local variables (lexical scope only) +- `binding` temporarily overrides global definitions (dynamic scope) + ```phel -(ns my-app\tests\demo - (:require phel\test :refer [deftest is])) +# Example 1: Simple binding demonstration +(def *config* "production") + +(defn get-config [] + *config*) + +(get-config) # => "production" + +# let doesn't affect the global definition +(let [*config* "test"] + (get-config)) # => "production" (still uses global!) + +# binding temporarily overrides the global definition +(binding [*config* "test"] + (get-config)) # => "test" (uses binding!) -# Function that would return e.g. "x86_64", depending on the environment: -(defn get-system-architecture [] (php/php_uname "m")) +(get-config) # => "production" (back to original) + +# Example 2: Mocking functions for testing +(defn get-system-architecture [] + (php/php_uname "m")) (defn greet-user-by-architecture [] - (print "Hello" (get-system-architecture) "user!")) + (str "Hello " (get-system-architecture) " user!")) + +# Without binding - calls actual system function +(greet-user-by-architecture) # => "Hello x86_64 user!" (or your system arch) -# Bindings with let are not effective outside it's lexical scope -(deftest greeting-test-let - (let [get-system-architecture |(str "i386")] # <- mock function - (let [greeting-out (with-output-buffer (greet-user-by-architecture))] - (is (= "Hello i386 user!" greeting-out) - "i386 system user is greeted accordingly")))) +# With let - doesn't work! Function still calls original +(let [get-system-architecture |(str "i386")] + (greet-user-by-architecture)) # => "Hello x86_64 user!" (original still used!) -# Test fails on a x86_64 system, evaluating to "Hello x86_64 user!": -# ✘ greeting-test-let: i386 system user is greeted accordingly +# With binding - successfully mocks the function +(binding [get-system-architecture |(str "i386")] + (greet-user-by-architecture)) # => "Hello i386 user!" (mocked!) + +# Example 3: Testing with binding +(ns my-app\tests\demo + (:require phel\test :refer [deftest is])) -# With binding, a mock function can bound in place of the original one (deftest greeting-test-binding - (binding [get-system-architecture |(str "i386")] # <- mock function - (let [greeting-out (with-output-buffer (greet-user-by-architecture))] - (is (= "Hello i386 user!" greeting-out) - "i386 system user is greeted accordingly")))) + (binding [get-system-architecture |(str "i386")] + (is (= "Hello i386 user!" (greet-user-by-architecture)) + "i386 system user is greeted accordingly"))) +# ✔ greeting-test-binding: i386 system user is greeted accordingly + +# Example 4: Multiple bindings at once +(def *db-host* "production-db") +(def *db-port* 5432) + +(defn connect [] + (str "Connecting to " *db-host* ":" *db-port*)) + +(binding [*db-host* "test-db" + *db-port* 3306] + (connect)) # => "Connecting to test-db:3306" + +(connect) # => "Connecting to production-db:5432" +``` + +{% php_note() %} +`binding` is useful for dependency injection and testing, similar to mocking frameworks in PHP: + +```php +// PHP - using dependency injection +class UserService { + public function __construct(private DbConnection $db) {} +} + +// In tests: +$mockDb = $this->createMock(DbConnection::class); +$service = new UserService($mockDb); -# Test is successful: -# ✔ greet-test-binding: i386 system user is greeted accordingly +// Phel - using binding (simpler for testing) +(defn get-user [id] + (query-db (str "SELECT * FROM users WHERE id=" id))) +(deftest test-get-user + (binding [query-db (fn [q] {:id 1 :name "Alice"})] + (is (= "Alice" (:name (get-user 1)))))) ``` +Binding is particularly useful for testing code that depends on global state or external systems. +{% end %} + +{% clojure_note() %} +`binding` works exactly like Clojure's `binding`—it creates dynamic scope bindings that affect all code executed within the binding form. +{% end %} + ## Variables ```phel @@ -105,3 +189,23 @@ To update a variable with a function the `swap!` function can be used. (swap! foo + 2) # Evaluates to 12 (deref foo) # Evaluates to 12 ``` + +{% php_note() %} +Variables provide mutable state similar to PHP variables, but are explicit and contained: + +```php +// PHP - everything is mutable by default +$count = 0; +$count++; + +// Phel - explicit mutability with variables +(def count (var 0)) +(swap! count inc) +``` + +Use Phel's immutable data structures when possible. Variables are mainly useful for interop with PHP code or managing application state. +{% end %} + +{% clojure_note() %} +Phel variables work like Clojure atoms—they're thread-safe containers for mutable state. Use `deref` or `@` to read, `swap!` to update. +{% end %} diff --git a/content/documentation/macros.md b/content/documentation/macros.md index d43ccbe9..b0a3277e 100644 --- a/content/documentation/macros.md +++ b/content/documentation/macros.md @@ -7,7 +7,7 @@ weight = 11 Macros are functions that take code as input and return transformed code as output. A macro is like a function that is executed at compile time. They are useful to extend the syntax of the language itself. -Phel's core library itself uses macro to define the language. For example `defn` is as macro. +Phel's core library uses macros to define the language. For example, `defn` is a macro. ```phel (defn add [a b] (+ a b)) @@ -17,6 +17,28 @@ is transformed to (def add (fn [a b] (+ a b))) ``` +{% php_note() %} +Macros are **not** like PHP functions. They run at compile-time and transform code before execution: + +```php +// PHP - No macro system +// You'd need to use code generation or eval() + +// Phel - Macros transform code at compile time +(defmacro unless [test then else] + `(if (not ,test) ,then ,else)) + +(unless false "yes" "no") # => "yes" +# Expands to: (if (not false) "yes" "no") at compile time +``` + +This is more powerful and safer than PHP's `eval()` or code generation. +{% end %} + +{% clojure_note() %} +Macros work exactly like Clojure macros—they transform code at compile time using quote, unquote, and syntax-quote. +{% end %} + ## Quote The quote operator is a special form, it returns its argument without evaluating it. Its purpose is to prevent any evaluation. Preceding a form with a single quote is a shorthand for `(quote form)`. @@ -59,3 +81,11 @@ For better readability of macros the `quasiquote` special form is defined. It tu ```phel (defmacro mydefn [name args & body] `(def ,name (fn ,args ,@body))) +``` + +{% clojure_note() %} +Quasiquote syntax works like Clojure: +- `` ` `` for quasiquote (syntax-quote) +- `,` for unquote +- `,@` for unquote-splicing +{% end %} diff --git a/content/documentation/namespaces.md b/content/documentation/namespaces.md index 65b4b069..b9e986fe 100644 --- a/content/documentation/namespaces.md +++ b/content/documentation/namespaces.md @@ -22,6 +22,34 @@ Defines the namespace for the current file and adds imports to the environment. The call also sets the `*ns*` variable to the given namespace. +{% php_note() %} +Phel namespaces are similar to PHP namespaces, but with key differences: + +```php +// PHP +namespace My\Custom\Module; +use Some\Php\Class; +use My\Phel\Module as Utilities; + +// Phel +(ns my\custom\module + (:use Some\Php\Class) + (:require my\phel\module :as utilities)) +``` + +**Key differences:** +- Phel uses `\` as separator (like PHP) +- `:require` for Phel modules, `:use` for PHP classes +- Functions/values are accessed with `/` not `::` +{% end %} + +{% clojure_note() %} +Namespaces work similarly to Clojure, but: +- Use `\` instead of `.` as separator (following PHP conventions) +- `:use` is for PHP classes (not Clojure vars) +- `:require` works like Clojure's `:require` +{% end %} + ### Import a Phel module Before a Phel module can be used, it has to be imported with the keyword `:require`. Once imported, the module can be accessed by its name followed by a slash and the name of the public function or value. Namespaces are indexed from source file directory which is `src/` by default and can changed with [SrcDirs configuration option](/documentation/configuration/#srcdirs) in `phel-config.php`. @@ -75,6 +103,10 @@ Additionally, it is possible to refer symbols of other modules in the current na Both, `:refer` and `:as` can be combined in any order. +{% clojure_note() %} +`:refer` works exactly like Clojure's `:refer`—imports specific symbols into the current namespace. +{% end %} + ### Import a PHP class PHP classes are imported with the keyword `:use`. diff --git a/content/documentation/php-interop.md b/content/documentation/php-interop.md index b8a515ca..34c40d2d 100644 --- a/content/documentation/php-interop.md +++ b/content/documentation/php-interop.md @@ -19,6 +19,24 @@ Named constants set with PHP [`define`](https://www.php.net/manual/en/function.d php/MY_SETTING # Returns "My value" ``` +{% php_note() %} +The `php/` prefix gives you direct access to PHP's global scope: + +```php +// PHP +$_SERVER['key'] +$GLOBALS['argv'] +MY_SETTING + +// Phel +(get php/$_SERVER "key") +(get php/$GLOBALS "argv") +php/MY_SETTING +``` + +**Note:** Use Phel's immutable data structures when possible. Only use PHP arrays when you need to interop with PHP libraries that expect them. +{% end %} + ## Calling PHP functions PHP comes with huge set of functions that can be called from Phel by just adding a `php/` prefix to the function name. @@ -28,6 +46,24 @@ PHP comes with huge set of functions that can be called from Phel by just adding (php/date "l") # Evaluates to something like "Monday" ``` +{% php_note() %} +Any PHP function can be called by adding the `php/` prefix: + +```php +// PHP +strlen("test"); +date("l"); +array_map($fn, $array); + +// Phel +(php/strlen "test") +(php/date "l") +(php/array_map fn array) +``` + +However, Phel provides functional equivalents for many operations. For example, use `(count "test")` instead of `(php/strlen "test")` when working with Phel data structures. +{% end %} + ## PHP class instantiation ```phel @@ -46,6 +82,24 @@ Evaluates `expr` and creates a new PHP class using the arguments. The instance o (php/new "\\DateTimeImmutable") # instantiate a new PHP class from string ``` +{% php_note() %} +Class instantiation in Phel uses `php/new` instead of PHP's `new` keyword: + +```php +// PHP +new DateTime(); +new DateTime("now"); +new \DateTimeImmutable(); + +// Phel +(php/new DateTime) +(php/new DateTime "now") +(php/new "\\DateTimeImmutable") +``` + +You can import classes with `:use` to avoid repeating the namespace, just like PHP's `use` statement. +{% end %} + ## PHP method and property call ```phel @@ -65,7 +119,7 @@ You can chain multiple method calls or property accesses in one `php/->` express (php/-> di (format "%s seconds")) # Evaluates to "30 seconds" (php/-> di s) # Evaluates to 30 -# Chain multiple calls: +# Chain multiple calls: # (new DateTimeImmutable("2024-03-10"))->modify("+1 day")->format("Y-m-d") (php/-> (php/new \DateTimeImmutable "2024-03-10") (modify "+1 day") @@ -82,6 +136,32 @@ You can chain multiple method calls or property accesses in one `php/->` express (php/-> user address city) # Evaluates to "Berlin" ``` +{% php_note() %} +The `php/->` operator is similar to PHP's `->` but allows chaining in a more functional style: + +```php +// PHP +$di->format("%s seconds"); +$di->s; +(new DateTimeImmutable("2024-03-10"))->modify("+1 day")->format("Y-m-d"); +$user->profile->getDisplayName(); + +// Phel +(php/-> di (format "%s seconds")) +(php/-> di s) +(php/-> (php/new \DateTimeImmutable "2024-03-10") + (modify "+1 day") + (format "Y-m-d")) +(php/-> user profile (getDisplayName)) +``` + +Method calls use parentheses `(methodname args)`, while property access is just the symbol name. +{% end %} + +{% clojure_note() %} +The `php/->` operator is inspired by Clojure's thread-first macro `->`, but specifically designed for PHP object method chaining. +{% end %} + ## PHP static method and property call ```phel @@ -99,8 +179,21 @@ Same as above, but for static calls on PHP classes. # Evaluates to a new instance of DateTimeImmutable (php/:: DateTimeImmutable (createFromFormat "Y-m-d" "2020-03-22")) +``` + +{% php_note() %} +The `php/::` operator is equivalent to PHP's `::` for static method and property access: +```php +// PHP +DateTimeImmutable::ATOM; +DateTimeImmutable::createFromFormat("Y-m-d", "2020-03-22"); + +// Phel +(php/:: DateTimeImmutable ATOM) +(php/:: DateTimeImmutable (createFromFormat "Y-m-d" "2020-03-22")) ``` +{% end %} ## PHP set object properties @@ -116,6 +209,22 @@ Use `php/oset` to set a value to a class/object property. (php/oset (php/-> x name) "foo") ``` +{% php_note() %} +`php/oset` is the Phel equivalent of PHP's property assignment: + +```php +// PHP +$x = new stdClass(); +$x->name = "foo"; + +// Phel +(def x (php/new \stdclass)) +(php/oset (php/-> x name) "foo") +``` + +**Note:** This mutates the PHP object. When possible, use Phel's immutable data structures instead. +{% end %} + ## Get PHP-Array value ```phel @@ -130,6 +239,26 @@ Equivalent to PHP's `arr[index] ?? null`. (php/aget (php/array "a" "b" "c") 5) # Evaluates to nil ``` +{% php_note() %} +`php/aget` safely accesses PHP array elements: + +```php +// PHP +$arr[0] ?? null; +$arr[1] ?? null; +$arr[5] ?? null; // Returns null + +// Phel +(php/aget arr 0) +(php/aget arr 1) +(php/aget arr 5) # Returns nil +``` + +**Important distinction:** +- Use `php/aget` for **PHP arrays** (mutable) +- Use `get` for **Phel data structures** (immutable vectors, maps) +{% end %} + ## Get nested PHP-Array value ```phel @@ -150,15 +279,33 @@ path is missing, `nil` is returned. (php/aget-in users ["users" 1 "name"]) # Evaluates to "Bob" -(php/aget-in - (php/array "meta" (php/array "status" "ok")) +(php/aget-in + (php/array "meta" (php/array "status" "ok")) ["meta" "status"]) # Evaluates to "ok" -(php/aget-in - (php/array "meta" (php/array "status" "ok")) +(php/aget-in + (php/array "meta" (php/array "status" "ok")) ["meta" "missing"]) # Evaluates to nil ``` +{% php_note() %} +`php/aget-in` provides safe nested array access: + +```php +// PHP - manual nested access with null coalescing +$users['users'][1]['name'] ?? null; +$data['meta']['status'] ?? null; +$data['meta']['missing'] ?? null; + +// Phel - clean path-based access +(php/aget-in users ["users" 1 "name"]) +(php/aget-in data ["meta" "status"]) +(php/aget-in data ["meta" "missing"]) # Returns nil safely +``` + +This is similar to Phel's `get-in` for immutable data structures, but specifically for PHP arrays. +{% end %} + ## Set PHP-Array value ```phel @@ -167,6 +314,20 @@ path is missing, `nil` is returned. Equivalent to PHP's `arr[index] = value`. +{% php_note() %} +`php/aset` mutates a PHP array in place: + +```php +// PHP +$arr[0] = "value"; + +// Phel +(php/aset arr 0 "value") +``` + +**Important:** This mutates the array. For immutable operations, use Phel's `assoc` on Phel data structures instead. +{% end %} + ## Set nested PHP-Array value ```phel @@ -183,6 +344,22 @@ created as needed to ensure the path exists before writing the value. # Equivalent to $data['user']['profile']['name'] = 'Charlie'; ``` +{% php_note() %} +`php/aset-in` creates nested structures automatically: + +```php +// PHP - manual nested array creation +$data = []; +$data['user']['profile']['name'] = 'Charlie'; + +// Phel - automatic path creation +(def data (php/array)) +(php/aset-in data ["user" "profile" "name"] "Charlie") +``` + +This is the mutable counterpart to Phel's `assoc-in` for immutable data structures. +{% end %} + ## Append PHP-Array value ```phel @@ -191,6 +368,20 @@ created as needed to ensure the path exists before writing the value. Equivalent to PHP's `arr[] = value`. +{% php_note() %} +`php/apush` appends to a PHP array: + +```php +// PHP +$arr[] = "new value"; + +// Phel +(php/apush arr "new value") +``` + +For immutable operations, use `conj` on Phel vectors instead. +{% end %} + ## Unset PHP-Array value ```phel @@ -199,6 +390,20 @@ Equivalent to PHP's `arr[] = value`. Equivalent to PHP's `unset(arr[index])`. +{% php_note() %} +`php/aunset` removes an element from a PHP array: + +```php +// PHP +unset($arr[0]); + +// Phel +(php/aunset arr 0) +``` + +For immutable operations, use `dissoc` on Phel maps instead. +{% end %} + ## Unset nested PHP-Array value ```phel @@ -215,6 +420,20 @@ remain untouched even if they become empty. # Equivalent to unset($data['user']['profile']['name']); ``` +{% php_note() %} +`php/aunset-in` removes nested array elements: + +```php +// PHP +unset($data['user']['profile']['name']); + +// Phel +(php/aunset-in data ["user" "profile" "name"]) +``` + +Parent arrays remain intact even if they become empty after the unset. +{% end %} + ## `__DIR__`, `__FILE__`, and `*file*` In Phel you can also use PHP Magic Methods `__DIR__` and `__FILE__`. When the @@ -235,6 +454,21 @@ directory. (println *file*) # Absolute path of the original file ``` +{% php_note() %} +**Important distinction:** + +```php +// PHP magic constants +__DIR__ // Points to .phel/cache directory (generated PHP) +__FILE__ // Points to cached .php file + +// Phel special var +*file* // Points to your actual .phel source file +``` + +Use `*file*` when you need to reference the original Phel source location, such as for loading resources relative to your source code. +{% end %} + ## Calling Phel functions from PHP Phel also provides a way to let you call Phel functions from PHP. This is useful for existing PHP application that wants to integrate Phel. diff --git a/content/documentation/testing.md b/content/documentation/testing.md index 077ad140..137382c6 100644 --- a/content/documentation/testing.md +++ b/content/documentation/testing.md @@ -5,6 +5,29 @@ weight = 17 Phel comes with an integrated unit testing framework. +{% php_note() %} +Unlike PHPUnit which uses classes and methods, Phel's testing is more lightweight: + +```php +// PHPUnit - class-based tests +class MyTest extends TestCase { + public function testAddition() { + $this->assertEquals(4, 2 + 2); + } +} + +// Phel - function-based tests +(deftest my-test + (is (= 4 (+ 2 2)))) +``` + +Phel's approach is simpler for functional code and doesn't require class boilerplate. +{% end %} + +{% clojure_note() %} +Phel's testing framework is modeled after `clojure.test`—same `is` macro, same `deftest` structure. +{% end %} + ## Assertions The core of the library is the `is` macro, which can be used to defined assertions. @@ -59,6 +82,35 @@ This tests whether the execution of `body` throws an exception of type `exceptio ``` This tests whether the execution of `body` prints the `expected` text to the output stream. +{% php_note() %} +Exception testing in Phel is more concise than PHPUnit: + +```php +// PHPUnit +$this->expectException(Exception::class); +throw new Exception("test"); + +// or +$this->expectException(Exception::class); +$this->expectExceptionMessage("test"); +throw new Exception("test"); + +// Phel - inline exception assertions +(is (thrown? \Exception (throw (php/new \Exception "test")))) +(is (thrown-with-msg? \Exception "test" (throw (php/new \Exception "test")))) +``` + +The `output?` assertion is similar to PHPUnit's output buffering: +```php +// PHPUnit +$this->expectOutputString("hello"); +echo "hello"; + +// Phel +(is (output? "hello" (php/echo "hello"))) +``` +{% end %} + ## Defining tests Test can be defined by using the `deftest` macro. This macro is like a function without arguments. @@ -91,6 +143,24 @@ Test report can be set to more verbose TestDox format showing individual test na See more options available by running `./vendor/bin/phel test --help`. +{% php_note() %} +Phel's test command is similar to PHPUnit: + +```bash +# PHPUnit +./vendor/bin/phpunit tests/ +./vendor/bin/phpunit tests/MainTest.php +./vendor/bin/phpunit --filter testMyFunction + +# Phel +./vendor/bin/phel test +./vendor/bin/phel test tests/main.phel +./vendor/bin/phel test --filter my-test-function +``` + +Both support filtering, verbose output, and running specific test files. +{% end %} + If you want to run tests from Phel code, the `run-tests` function can be used. As arguments, it takes a map of options (that can be empty) and one or more namespaces that should be tested. diff --git a/content/documentation/truth-and-boolean-operations.md b/content/documentation/truth-and-boolean-operations.md index 4806503b..14aeec03 100644 --- a/content/documentation/truth-and-boolean-operations.md +++ b/content/documentation/truth-and-boolean-operations.md @@ -3,7 +3,11 @@ title = "Truth and Boolean operations" weight = 4 +++ -Phel has a different concept of truthiness. In Phel, only `false` and `nil` represent falsity. Everything else evaluates to true. The function `truthy?` can be used to check if a value is truthy. To check for the values `true` and `false` the functions `true?` and `false?` can be used. +## Truthiness + +In Phel, only `false` and `nil` represent falsity. Everything else evaluates to true—including `0`, `""`, and `[]`. + +The function `truthy?` can be used to check if a value is truthy. To check for the values `true` and `false` specifically, the functions `true?` and `false?` can be used. ```phel (truthy? false) # Evaluates to false @@ -23,6 +27,26 @@ Phel has a different concept of truthiness. In Phel, only `false` and `nil` repr (false? -1) # Evaluates to false ``` +{% php_note() %} +This is **different from PHP** where `0`, `""`, `[]`, and `null` are all falsy. + +```php +// PHP +if (0) { } // false - won't execute +if ("") { } // false - won't execute +if ([]) { } // false - won't execute + +// Phel +(if 0 "yes" "no") # => "yes" - 0 is truthy! +(if "" "yes" "no") # => "yes" - "" is truthy! +(if [] "yes" "no") # => "yes" - [] is truthy! +``` +{% end %} + +{% clojure_note() %} +Truthiness works exactly like Clojure—only `false` and `nil` are falsy. +{% end %} + ## Identity vs Equality The function `id` returns `true` if two values are identical. Identity is stricter than equality. It first checks if both types are identical and then compares their values. Phel keywords and symbols with the same names are always identical. Lists, vectors, maps and sets are only identical if they point to the same references. @@ -53,7 +77,27 @@ To check if two values are equal, the equal function (`=`) can be used. Two valu (= {} {}) # Evaluates to true ``` -The function `id` is equivalent to PHP's identity operator (`===`) with support for Phel types. However, the equality function `=` is not equivalent to PHP's equal operator (`==`). If you want to test if two values are PHP equal, the function `php/==` can be used. To check if two values are unequal the `not=` function can be used. +To check if two values are unequal, the `not=` function can be used. + +{% php_note() %} +**Comparison with PHP operators:** + +- `id` is like PHP's `===` (identity/strict equality) with support for Phel types +- `=` is **not** like PHP's `==` (loose equality) +- `=` compares Phel values structurally with type checking + +If you need PHP's loose equality, use `php/==`: + +```phel +(php/== 5 "5") # => true (PHP loose equality) +(= 5 "5") # => false (Phel structural equality) +(id 5 5) # => true (Phel identity) +``` +{% end %} + +{% clojure_note() %} +`id` is like Clojure's `identical?`, and `=` is like Clojure's `=`. +{% end %} ## Comparison Operations diff --git a/css/components/documentation.css b/css/components/documentation.css index f837df96..c65feea3 100644 --- a/css/components/documentation.css +++ b/css/components/documentation.css @@ -62,6 +62,71 @@ h6:hover .zola-anchor { color: var(--color-light-link); } +/* Developer Notes (Collapsible) */ +.dev-note { + margin: var(--space-lg) 0; + padding: var(--space-md); + border-radius: var(--radius-md); + border-left: 4px solid; + background-color: var(--color-light-surface); +} + +.dev-note summary { + cursor: pointer; + font-weight: 600; + font-size: var(--text-base); + user-select: none; + list-style: none; + padding: var(--space-sm); + margin: calc(-1 * var(--space-sm)); + border-radius: var(--radius-sm); + transition: background-color var(--duration-fast) var(--ease-out); +} + +.dev-note summary::-webkit-details-marker { + display: none; +} + +.dev-note summary::before { + content: '▶'; + display: inline-block; + margin-right: var(--space-sm); + transition: transform var(--duration-fast) var(--ease-out); +} + +.dev-note[open] summary::before { + transform: rotate(90deg); +} + +.dev-note summary:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.dev-note-content { + margin-top: var(--space-md); + padding-top: var(--space-md); +} + +.dev-note-content > *:first-child { + margin-top: 0; +} + +.dev-note-content > *:last-child { + margin-bottom: 0; +} + +/* PHP Note Styling */ +.php-note { + border-left-color: #8892bf; /* PHP purple/blue */ + background-color: #f8f9ff; +} + +/* Clojure Note Styling */ +.clojure-note { + border-left-color: #63b132; /* Clojure green */ + background-color: #f6faf3; +} + /* ======================================== DARK MODE ======================================== */ @@ -74,3 +139,21 @@ h6:hover .zola-anchor { .dark .zola-anchor:hover { color: var(--color-dark-anchor); } + +.dark .dev-note { + background-color: rgba(255, 255, 255, 0.05); +} + +.dark .dev-note summary:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.dark .php-note { + background-color: rgba(136, 146, 191, 0.1); + border-left-color: #a0a8d4; +} + +.dark .clojure-note { + background-color: rgba(99, 177, 50, 0.1); + border-left-color: #7bc950; +} diff --git a/static/scroll-active-nav.js b/static/scroll-active-nav.js new file mode 100644 index 00000000..7f428f01 --- /dev/null +++ b/static/scroll-active-nav.js @@ -0,0 +1,28 @@ +/** + * Scroll the active navigation item into view on page load + */ +(function() { + 'use strict'; + + function scrollActiveNavIntoView() { + // Find the active navigation item in the sidebar + const activeNavItem = document.querySelector('.site-navigation__entry.active'); + + if (activeNavItem) { + // Scroll it into view with some padding + activeNavItem.scrollIntoView({ + behavior: 'auto', + block: 'center', + inline: 'nearest' + }); + } + } + + // Run when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', scrollActiveNavIntoView); + } else { + // DOM is already ready + scrollActiveNavIntoView(); + } +})(); diff --git a/templates/base.html b/templates/base.html index 573e1619..01fc7fae 100644 --- a/templates/base.html +++ b/templates/base.html @@ -45,6 +45,7 @@ + diff --git a/templates/shortcodes/clojure_note.html b/templates/shortcodes/clojure_note.html new file mode 100644 index 00000000..6ef38590 --- /dev/null +++ b/templates/shortcodes/clojure_note.html @@ -0,0 +1,6 @@ +
+ 🌿 For Clojure/Lisp developers +
+ {{ body | markdown | safe }} +
+
diff --git a/templates/shortcodes/php_note.html b/templates/shortcodes/php_note.html new file mode 100644 index 00000000..7567d73f --- /dev/null +++ b/templates/shortcodes/php_note.html @@ -0,0 +1,6 @@ +
+ 📘 For PHP developers +
+ {{ body | markdown | safe }} +
+