mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2024-11-29 22:12:07 +03:00
776 lines
26 KiB
Markdown
776 lines
26 KiB
Markdown
|
---
|
||
|
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") ; => #<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.
|