From ddde3166a8eecffcfbaac6fc1e7d28aac243adf3 Mon Sep 17 00:00:00 2001 From: Jesse Wattenbarger Date: Wed, 11 Sep 2024 06:06:26 -0400 Subject: [PATCH] [fennel/en] Introduce fennel doc --- fennel.html.markdown | 775 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 775 insertions(+) create mode 100644 fennel.html.markdown diff --git a/fennel.html.markdown b/fennel.html.markdown new file mode 100644 index 00000000..b6331a63 --- /dev/null +++ b/fennel.html.markdown @@ -0,0 +1,775 @@ +--- +language: fennel +filename: learnfennel.fnl +contributors: + - ["Jesse Wattenbarger", "https://github.com/jjwatt"] +--- + +Fennel is a programming language that brings together the simplicity, +speed, and reach of Lua with the flexibility of a lisp syntax and +macro system. + + +```fennel +;; Comments start with semicolons. + +;; Fennel is written in lists of things inside parentheses, separated +;; by whitespace. + +;; The first thing in parentheses is a function or macro to call, and +;; the rest are the arguments. + +;; ------------------------- ;; +;; 1. Primitives & Operators ;; +;; ------------------------- ;; + +;; (local ...) defines a var inside the whole file's scope. +(local s "walternate") ; Immutable strings like Python. + +;; local supports destructuring and multiple value binding. +;; (covered later). + +;; Strings are utf8 byte arrays +"λx:(μα.α→α).xx" ; can include Unicode characters + +;; .. will create a string out of it's arguments. +;; It will coerce numbers but nothing else. +(.. "Hello" " " "World") ; => "Hello World" + +;; (print ...) will print all arguments with tabs in between +(print "Hello" "World") ; "Hello World" printed to screen + +(local num 42) ;; Numbers can be integer or floating point. + +;; Equality is = +(= 1 1) ; => true +(= 2 1) ; => false + +;; Nesting forms works as you expect +(+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2 + +;; Comparisons +(> 1 2) ; => false +(< 1 2) ; => true +(>= 1 1) ; => true +(<= 1 2) ; => true +(not= 1 2) ; => true + +;; TODO: find some bitwise operator examples +;; (lshift 1) ; => 2 + +;; -------- ;; +;; 2. Types ;; +;; -------- ;; + +;; Fennel uses Lua's types for booleans, strings & numbers. +;; Use `type` to inspect them. +(type 1) ; => "number" +(type 1.0) ; => "number" +(type "") ; => "string" +(type false) ; => "boolean" +(type nil) ; => "nil" + +;; Booleans +true ; for true +false ; for false +(not true) ; => false +(and 0 false) ; => false +(or false 0) ; => 0 + +;; All values other than nil or false are treated as true. + +;;,-------- +;;| Binding +;;`-------- + +;; Use `let` to bind local vars to values. +;; Local binding: `me` is bound to "Bob" only within the (let ...) +(let [me "Bob"] + (print "returning Bob") + me) ; => "Bob" +;; Outside the body of the let, the bindings it introduces are no +;; longer visible. The last form in the body is used as the return +;; value. `set` does not work on `let` bound locals. + +;; `local` introduces a new local inside an existing scope. Similar to +;; let but without a body argument. Recommended for use at the +;; top-level of a file for locals which will be used throughout the +;; file. `set` does not work on `locals` +(local tau-approx 6.28318) + +;; `var` introduces a new local inside an existing scope which may have its +;; value changed. Identical to local apart from allowing set to work +;; on it. `set` works on `vars` +(var x 83) + +;; `set` local variable or table field +;; Changes the value of a variable introduced with `var`. Will not work +;; on `globals` or `let`/`local`-bound locals. +(set x (+ x 91)) ; var + +;; Can also be used to change a field of a table, even if the table is +;; bound with `let` or `local`. If the table field name is static, use +;; `tbl.field`; if the field name is dynamic, use `(. tbl field)`. +(let [t {:a 4 :b 8}] ; static table field + (set t.a 2) t) ; => {:a 2 :b 8} + +;; In any context where you can make a new binding, you can use +;; multiple value binding. Otherwise, you will only capture the first +;; value. +(let [x (values 1 2 3)] + x) ; => 1 + +;; `global` set global variable +;; Sets a global variable to a new value. Note that there is no +;; distinction between introducing a new global and changing the value +;; of an existing one. This supports destructuring and multiple-value +;; binding. +(global tbl {:a 4 :b 8}) +(global prettyprint (fn [x] (print (fennel.view x)))) + +;;,-------------------------- +;;| Prettyprint and .fennelrc +;;`-------------------------- +;; `prettyprint` is a good function to have in your repl + +;; Return values in the repl will get pretty-printed, but calling +;; (print tbl) will emit output like table: 0x55a3a8749ef0. If you +;; don't already have one, it's recommended for debugging to define a +;; printer function which calls `fennel.view` on its argument before +;; printing it. +(local fennel (require :fennel)) +(fn _G.pp [x] (print + (fennel.view x))) +;; Notice that adding it to `_G` puts the function in the global +;; table, similar to what `global` would do. The fennel documentation +;; says that using `_G` is the preferred method of defining and using +;; globals. +;; If you add this definition to your ~/.fennelrc +;; file it will be available in the standard repl. +(pp tbl) + +;;,-------------------------------- +;;| Collections & Sequences: Tables +;;`-------------------------------- + +;; Tables are the only compound data structure in Lua and fennel. +;; Similar to php arrays or js objects, they are +;; hash-lookup dicts that can also be used as lists. + +;; tables can be treated as sequential or non-sequential: as hashmaps +;; or lists/arrays. + +;; Using tables as dictionaries / maps: +(local t {:key1 "value1" :key2 false}) + +;; String keys can use dot notation: +(print t.key1) ;; Prints "value1" + +;; Setting table keys and values +(tset t :newKey {}) ;; Adds a new key/value pair. +(tset t :key2 nil) ;; Removes key2 from the table. +;; Literal notation for any (non-nil) value as key + +;; length string or table length +(+ (length [1 2 3 nil 8]) (length "abc")) ; => 8 + +;; . table lookup looks up a given key in a table. Multiple arguments +;; will perform nested lookup. +(. t :key1) + +(let [t {:a [2 3 4]}] (. t :a 2)) ; => 3 + +;; If the field name is a string known at compile time, you don't need +;; this and can just use table.field (dot notation). + +;; Nil-safe ?. table lookup +;; Looks up a given key in a table. Multiple arguments will perform +;; nested lookup. If any subsequent keys is not presnet, will +;; short-circuit to nil. +(?. t :key1) ; => "value" +(let [t {:a [2 3 4]}] (?. t :a 4 :b)) ; => nil +(let [t {:a [2 3 4 {:b 42}]}] (?. t :a 4 :b)) ; => 42 + +;; The table module +(let [t [1 2 3]] + (table.insert t 2 "a") ; t is now [1 "a" 2 3] + (table.insert t "last") ; now [1 "a" 2 3 "last"] + (print (table.remove t)) ; prints "last" + (table.remove t 1) ; t is now ["a" 2 3] + (print (table.concat t ", "))) ; prints "a, 2, 3" + +;; The table.sort function sorts a table in-place, as a +;; side-effect. It takes an optional comparator function which should +;; return true when its first argument is less than the second. + +;; The table.unpack function returns all the elements in the table as +;; multiple values. Note that table.unpack is just unpack in Lua 5.1. +;; This will make `unpack` work in both. +(var unpack (or _G.unpack table.unpack)) +;; See "prettyprint" section about printing tables + + +;; --------------------- ;; +;; 3. Basic Flow Control ;; +;; --------------------- ;; +;; `if` checks a condition and evaluates the corresponding body. +;; Accepts any number of condition/body pairs. If an odd number of +;; args is given, the last value is treated as a catch-all "else," +;; similar to cond in other lisps. +(let [x (math.random 64)] + (if (= 0 (% x 10)) + "multiple of ten" + (= 0 (% x 2)) + "even" + "I dunno, something else")) +;; All values other than nil or false are treated as true. + +;; `when` takes a single condition and evalutes the rest as a body if +;; it's truthy. Intended for side-effects. The last form is the return +;; value. +(when launch-missiles? + (power-on) + (open-doors) + (fire)) + +;;,------------------ +;;| Loops & Iteration +;;`------------------ + +;; each: general iteration +;; `each` runs the body once for each value provided by the iterator. +(each [key value (pairs mytbl)] + (print "executing key") + (print (f value))) + +;; Any loop can be terminated early by placing an &until clause at the +;; end of the bindings +(local out []) +(each [_ value (pairs tbl) &until (< max-len (length out))] + (table.insert out value)) + +;; `for` is a numeric loop with start, stop and optional step. +(for [i 1 10 2] + (log-number i) + (print i)) ;; print odd numbers under 10 + +;; Like each, loops using for can also be terminated early with an +;; &until clause +(var x 0) +(for [i 1 128 &until (maxed-out? x)] + (set x (+ x i))) + +;; while loops over a body until a condition is met +;; Returns nil. +(var done? false) +(while (not done?) + (print :not-done) + (when (< 0.95 (math.random)) + (set done? true))) +;; while uses the native lua while loop + +;; `do` evaluate multiple forms returning last value +;; Accepts any number of forms and evaluates all of them in order, +;; returning the last value. This is used for inserting side-effects +;; into a form which accepts only a single value, such as in a body of +;; an if when multiple clauses make it so you can't use when. Some +;; lisps call this begin or progn. +(if launch-missiles? + (do + (power-on) + (open-doors) + (fire)) + false-alarm? + (promote lt-petrov)) + +;; Many functions and macros like fn & let have an implicit do at the +;; start, so you don't have to add it to use multiple forms. + +;;,----------------------- +;;| `collect` & `icollect` +;;`----------------------- +;; icollect, collect: table comprehension macros + +;; The icollect macro takes a "iterator binding table" in the same +;; format that `each` takes, and returns a sequential table containing +;; all the values produced by each iteration of the macro's body. This +;; is similar to how map works in several other languages, but it is a +;; macro, not a function. + +;; If the value is nil, it is omitted from the return table. This is +;; analogous to filter in other languages. +(icollect [_ v (ipairs [1 2 3 4 5 6])] + (if (< 2 v) (* v v))) +;; -> [9 16 25 36] +;; equivalent to: +(let [tbl []] + (each [_ v (ipairs [1 2 3 4 5 6])] + (tset tbl (+ (length tbl) 1) (if (< 2 v) (* v v)))) + tbl) + +;; The `collect` macro is almost identical, except that the body should +;; return two things: a key and a value. +(collect [k v (pairs {:apple "red" :orange "orange" :lemon "yellow"})] + (if (not= v "yellow") + (values (.. "color-" v) k))) +;; -> {:color-orange "orange" :color-red "apple"} +;; equivalent to: +(let [tbl {}] + (each [k v (pairs {:apple "red" :orange "orange"})] + (if (not= v "yellow") + (match (values (.. "color-" v) k) + (key value) (tset tbl key value)))) + tbl) + +;; If the key and value are given directly in the body of collect and +;; not nested in an outer form, then the `values` call can be omitted +;; for brevity +(collect [k v (pairs {:a 85 :b 52 :c 621 :d 44})] + k (* v 5)) +;; -> {:a 425 :b 260 :c 3105 :d 220} + +;; If the index and value are given directly in the body of collect and +;; not nested in an outer form, then the values can be omitted for +;; brevity +(icollect [_ x (ipairs [2 3]) &into [9]] + (* x 11)) +;; -> [9 22 33] + +;; accumulate +;; Runs through an iterator and performs accumulation, similar to fold +;; and reduce commonly used in functional programming languages. Like +;; collect and icollect, it takes an iterator binding table and an +;; expression as its arguments. The difference is that in accumulate, +;; the first two items in the binding table are used as an +;; "accumulator" variable and its initial value. For each iteration +;; step, it evaluates the given expression and its value becomes the +;; next accumulator variable. accumulate returns the final value of +;; the accumulator variable. +(accumulate [sum 0 + i n (ipairs [10 20 30 40])] + (+ sum n)) ; -> 100 + +;; `faccumulate` range accumulation Identical to accumulate, but +;; instead of taking an iterator and the same bindings as `each`, it +;; accepts the same bindings as `for` and will iterate the numerical +;; range. Accepts `&until` just like `for` and `accumulate`. +(faccumulate [n 0 i 1 5] (+ n i)) ; => 15 + +;; `fcollect` range comprehension Similarly to `icollect`, `fcollect` +;; provides a way of building a sequential table. Unlike `icollect`, +;; instead of an iterator it traverses a range, as accepted by the +;; `for` special. The `&into` and `&until` clauses work the same as in +;; `icollect.` +(fcollect [i 0 10 2] + (if (> i 2) (* i i))) +;; -> [16 36 64 100] +;; equivalent to: +(let [tbl {}] + (for [i 0 10 2] + (if (> i 2) + (table.insert tbl (* i i)))) + tbl) + +;; `values` Returns multiple values from a function. Usually used to +;; signal failure by returning nil followed by a message. +(fn [filename] + (if (valid-file-name? filename) + (open-file filename) + (values nil (.. "Invalid filename: " filename)))) + +;; See the Destructuring and Matching sections for more advanced Flow +;; Control. + +;; ------------ ;; +;; 4. Functions ;; +;; ------------ ;; + +;; Use fn to create new functions. A function always returns its last +;; statement. +(fn [] "Hello World") ; => # + +; (You need extra parens to call it) +((fn [] "Hello World")) ; => "Hello World" + +;; Assign a function to a local variable +(local hello-world (fn [] "Hello World")) +(hello-world) ; => "Hello World" + +;; You can use fn and name a function. +(fn hello-world [] "Hello World") ; => "Hello World" + +;; The [] is the list of arguments to the function. +(fn hello [name] + (.. "Hello " name)) +(hello "Steve") ; => "Hello Steve" + +;; Will accept any number of arguments. ones in excess of the declared +;; ones are ignored, and if not enough arguments are supplied to cover +;; the declared ones, the remaining ones are given values of nil. + +;; Providing a name that's a table field will cause it to be inserted +;; in a table instead of bound as a local +(local functions {}) + +(fn functions.p [x y z] + (print (* x (+ y z)))) + +;; equivalent to: +(set functions.p (fn [x y z] + (print (* x (+ y z))))) + +;; Like Lua, functions in Fennel support tail-call optimization, +;; allowing (among other things) functions to recurse indefinitely +;; without overflowing the stack, provided the call is in a tail +;; position. +(fn factorial [x acc] + (if (= 0 x) + acc + (factorial (- x 1) (* x acc)))) +(factorial 5 1) ;; -> 120 +;; The final form in this and all other function forms is used as the +;; return value. + +;; (lambda [...]) +;; Creates a function like fn does, but throws an error at runtime if +;; any of the listed arguments are nil, unless its identifier begins +;; with ?. +(lambda [x ?y z] + (print (- x (* (or ?y 1) z)))) + +;; Note that the Lua runtime will fill in missing arguments with nil +;; when they are not provided by the caller, so an explicit nil +;; argument is no different than omitting an argument. + +;; Docstrings and metadata +;; The fn, lambda, λ and macro forms accept an optional docstring. +(fn pxy [x y] + "Print the sum of x and y" + (print (+ x y))) + +;; Hash function literal shorthand + +;; hashfn is a special function that you can abbreviate as # +;; #foo expands to (hashfn foo) + +;; Hash functions are anonymous functions of one form, with implicitly +;; named arguments. + +#(+ $1 $2) ;; same as +(hashfn (+ $1 $2)) ; implementation detail; don't use directly +;; same as +(fn [a b] (+a b)) + +;; A lone $ in a hash function is treated as ana alias for $1. +#(+ $ 1) + +#$ ; same as (fn [x] x) (aka the identity function +#val ; same as (fn [] val) +#[$1 $2 $3] ; same as (fn [a b c] [a b c]) + +;; ---------------------------------------------;; +;; 5. Destructuring, Binding & Pattern Matching ;; +;; ---------------------------------------------;; +;; Any time you bind a local, you can destructure it if the value is a +;; table or a function call which returns multiple values +(let [(x y z) (unpack [10 9 8])] + (+ x y z)) ; => 27 + +(let [[a b c] [1 2 3]] + (+ a b c)) ; => 6 +;; If a table key is a string with the same name as the local you want +;; to bind to, you can use shorthand of just : for the key name +;; followed by the local name. This works for both creating tables and +;; destructuring them. +(let [{:msg message : val} {:msg "hello there" :val 19}] + (print message) + val) ; prints "hello there" and returns 19 + +;; When destructuring a sequential table, you can capture all the +;; remainder of the table in a local by using & +(let [[a b & c] [1 2 3 4 5 6]] + (table.concat c ",")) ; => "3,4,5,6" + +;; When destructuring a non-sequential table, you can capture the +;; original table along with the destructuring by using &as +(let [{:a a :b b &as all} {:a 1 :b 2 :c 3 :d 4}] + (+ a b all.c all.d)) ; => 10 + +;;,----------------------- +;;| Multiple value binding +;;`----------------------- +;; In most contexts where you can make a new binding, you can use +;; multiple value binding. +(let [x (values 1 2 3)] x) ; = > 1 +(let [(file-handle message code) (io.open "fooblah.blah")] + message) ; => "fooblah.blah: No such file or directory" +(do + (local (_ _ z) (table.unpack [:a :b :c :d :e])) + z) ; => c + +;; tset sets the field of a given table to a new value. +(let [tbl {:d 32} + field :d] + (tset tbl field 19) tbl) ; => {:d 19} +;; You can provide multiple successive field names to perform +;; nested sets. +(let [tbl {:a + {:b {}}} + field :c] + (tset tbl :a :b field "d") tbl) ; => .. .. {:a {:b {:c "d"}}} + +;;,------------------------ +;;| `case` pattern matching +;;`------------------------ +;; Evaluates its first argument, then searches thru the subsequent +;; pattern/body clauses to find one where the pattern matches the +;; value, and evaluates the corresponding body. Pattern matching can +;; be thought of as a combination of destructuring and conditionals. +(case mytable + 59 :will-never-match-hopefully + [9 q 5] (print :q q) + [1 a b] (+ a b))q + +;; Patterns can be tables, literal values, or symbols. Any symbol is +;; implicitly checked to be not nil. Symbols can be repeated in an +;; expression to check for the same value. +(case mytable + ;; the first and second values of mytable are not nil and are the same value + [a a] (* a 2) + ;; the first and second values are not nil and are not the same value + [a b] (+ a b)) + +;; It's important to note that expressions are checked in order! In +;; the above example, since [a a] is checked first + +;; You may allow a symbol to optionally be nil by prefixing it with ?. +(case mytable + ;; not-nil, maybe-nil + [a ?b] :maybe-one-maybe-two-values + ;; maybe-nil == maybe-nil, both are nil or both are the same value + [?a ?a] :maybe-none-maybe-two-same-values + ;; maybe-nil, maybe-nil + [?a ?b] :maybe-none-maybe-one-maybe-two-values) + +;; Symbols prefixed by an _ are ignored and may stand in as positional +;; placeholders or markers for "any" value - including a nil value. A +;; single _ is also often used at the end of a case expression to +;; define an "else" style fall-through value. +(case mytable + ;; not-nil, anything + [a _b] :maybe-one-maybe-two-values + ;; anything, anything (different to the previous ?a example!) + ;; note this is effectively the same as [] + [_a _a] :maybe-none-maybe-one-maybe-two-values + ;; anything, anything + ;; this is identical to [_a _a] and in this example would never actually match. + [_a _b] :maybe-none-maybe-one-maybe-two-values + ;; when no other clause matched, in this case any non-table value + _ :no-match) + +;; You can match with multiple return values with +;; parenthesis, like you can with destructuring `let` +(case (io.open "/some/file") + (nil msg) (report-error msg) + f (read-file f)) + +;;,-------------- +;;| Guard Clauses +;;`-------------- +;; If you need to match on something more general than +;; a structure, use guard clauses: +(case [91 12 53] + (where [a b c] (= 5 a)) :will-not-match + (where [a b c] + (= 0 (math.fmod + (+ a b c) 2)) + (= 91 a)) + c) ; -> 53 +;; Each form after the pattern is a condition. All conditions must +;; evaluate to true for the pattern to match. + +;; If several patterns share the same body & guards, such patterns can +;; be with the `or` special in the `where` clause. +(case [5 1 2] + (where (or [a 3 9] [a 1 2]) (= 5 a)) + "Either [5 3 9] or [5 1 2]" + _ "anything else") + +;; Symbols bound inside a case pattern are independent from any +;; existing symbols in the current scope, Sometimes it may be +;; desirable to match against an existing value in the outer scope. To +;; do this we can "pin" a binding inside the pattern with an existing +;; outer binding with the unary (= binding-name) form. The unary (= +;; binding-name) form is only valid in a case pattern and must be +;; inside a (where) guard. +(let [x 1] + (case [1] + ;; 1 == 1 + (where [(= x)]) x + _ :no-match)) ; -> 1 +;; Pinning is only required inside the pattern. Outer bindings are +;; automatically available inside guards and bodies as long as the +;; name has not been rebound in the pattern. + +;; Note: The case macro can be used in place of the if-let macro from +;; Clojure. The reason Fennel doesn't have if-let is that case makes +;; it redundant. + +;;,------------------------- +;;| `match` pattern matching +;;`------------------------- +;; match is conceptually equivalent to case, except symbols in the +;; patterns are always pinned with outer-scope symbols if they exist. +(let [x 95] + (match [52 85 95] + [b a a] :no ; because a=85 and a=95 + [x y z] :no ; because x=95 and x=52 + [a b x] :yes)) ; a and b are fresh values while x=95 and x=95 + +;;,----------------------------------------------------- +;;| `case-try` & `match-try` for matching multiple steps +;;`----------------------------------------------------- +;; Evaluates a series of pattern matching steps. The value from the +;; first expression is matched against the first pattern. If it +;; matches, the first body is evaluated and its value is matched +;; against the second pattern, etc. +;; +;; If there is a (catch pat1 body1 pat2 body2 ...) form at the end, +;; any mismatch from the steps will be tried against these patterns in +;; sequence as a fallback just like a normal case. If no catch pattern +;; matches, nil is returned. +;; +;; If there is no catch, the mismatched value will be returned as the +;; value of the entire expression. +(fn handle [conn token] + (case-try (conn:receive :*l) + input (parse input) + (command-name params (= token)) (commands.get command-name) + command (pcall command (table.unpack params)) + (catch + (_ :timeout) nil + (_ :closed) (pcall disconnect conn "connection closed") + (_ msg) (print "Error handling input" msg)))) + +;; This is useful when you want to perform a series of steps, any of +;; which could fail. The catch clause lets you keep all your error +;; handling in one place. Note that there are two ways to indicate +;; failure in Fennel and Lua: using the assert/error functions or +;; returning nil followed by some data representing the failure. This +;; form only works on the latter, but you can use pcall to transform +;; error calls into values. + +;; `match-try` for matching multiple steps +;; Unlike case-try, match-try will pin values in a given catch block +;; with those in the original steps. +(fn handle [conn token] + (match-try (conn:receive :*l) + input (parse input) + (command-name params token) (commands.get command-name) + command (pcall command (table.unpack params)) + (catch + (_ :timeout) nil + (_ :closed) (pcall disconnect conn "connection closed") + (_ msg) (print "Error handling input" msg)))) + + +;; ---------;; +;; 6. Other ;; +;; ---------;; + +;; The `:` method call +;; Looks up a function in a table and calls it with the table as its +;; first argument. This is a common idiom in many Lua APIs, including +;; some built-in ones. Just like Lua, you can perform a method call by +;; calling a function name where : separates the table variable and +;; method name. +(let [f (assert (io.open "hello" "w"))] + (f:write "world") + (f:close)) + +;; If the name of the method or the table containing it isn't fixed, +;; you can use : followed by the table and then the method's name to +;; allow it to be a dynamic string instead +(let [f (assert (io.open "hello" "w")) + method1 :write + method2 :close] + (: f method1 "world") + (: f method2)) + +;; Unlike Lua, there's nothing special about defining functions that +;; get called this way; typically it is given an extra argument called +;; self but this is just a convention; you can name it anything. +(local t {}) +(fn t.enable [self] + (set self.enabled? true)) +(t:enable) + + +;; ->, ->>, -?> and -?>> threading macros The -> macro takes its first +;; value and splices it into the second form as the first +;; argument. The result of evaluating the second form gets spliced +;; into the first argument of the third form, and so on. +(-> 52 + (+ 91 2) ; (+ 52 91 2) + (- 8) ; (- (+ 52 91 2) 8) + (print "is the answer")) ; (print (- (+ 52 91 2) 8) "is the answer") + +;; The ->> macro works the same, except it splices it into the last +;; position of each form instead of the first. -?> and -?>>, the +;; thread maybe macros, are similar to -> & ->> but they also do +;; checking after the evaluation of each threaded form. If the result +;; is false or nil then the threading stops and the result is +;; returned. -?> splices the threaded value as the first argument, +;; like ->, and -?>> splices it into the last position, like ->>. +;; This example shows how to use them to avoid accidentally indexing a +;; nil value +(-?> {:a {:b {:c 42}}} + (. :a) + (. :missing) + (. :c)) ; -> nil +(-?>> :a + (. {:a :b}) + (. {:b :missing}) + (. {:c 42})) ; -> nil +;; While -> and ->> pass multiple values thru without any trouble, the +;; checks in -?> and -?>> prevent the same from happening there +;; without performance overhead, so these pipelines are limited to a +;; single value. + +;; doto +;; Similarly, the doto macro splices the first value into subsequent +;; forms. However, it keeps the same value and continually splices the +;; same thing in rather than using the value from the previous form +;; for the next form. +(doto (io.open "/tmp/err.log") + (: :write contents) + (: :close)) +;; equivalent to: +(let [x (io.open "/tmp/err.log")] + (: x :write contents) + (: x :close) + x) + +;; tail! +;; the tail! form asserts that its argument is called in a tail +;; position. You can use this when the code depends on tail call +;; optimization; that way if the code is changed so that the recursive +;; call is no longer in the tail position, it will cause a compile +;; error instead of overflowing the stack later on large data sets. +(fn process-all [data i] + (case (process (. data i)) + :done (print "Process completed.") + :next (process-all data (+ i 1)) + :skip (do (tail! (process-all data (+ i 2))) +;; ^^^^^ Compile error: Must be in tail position + (print "Skipped" (+ i 1))))) + +``` +### Further Reading + +The [fennel website] (https://fennel-lang.org/) is the best resource +on fennel. It links to the [fennel setup guide] +(https://fennel-lang.org/setup) and to the [fennel reference manual] +(https://fennel-lang.org/reference). This docuement borrows heavily in +parts from the fennel reference manual.