learnxinyminutes-docs/fennel.html.markdown
2024-09-14 23:08:49 -04:00

26 KiB

language filename contributors
fennel learnfennel.fnl
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.

;; 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") ; => #<function: 0x55630f9d7f20>

; (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.