diff options
| author | Andrey Orst <andreyorst@gmail.com> | 2021-01-19 16:46:00 +0000 |
|---|---|---|
| committer | Andrey Orst <andreyorst@gmail.com> | 2021-01-19 16:46:00 +0000 |
| commit | 92812d03922fd4c3ba85364b59e63236e7cfcd8a (patch) | |
| tree | 6504ecb114419b0dd3055e509abd8b602586f614 /doc/macros.md | |
| parent | a2b08f721c28b3b56a802031bc35df6a68b219d8 (diff) | |
| parent | 270beed0505ef47159d94fb162ff4840958f3ce5 (diff) | |
fix: Fennel 0.8.0 enhancements
Changelog:
- fixed bug in try
- reworked pretty printing for sets
- handle cycles in sets
- use new fennel.view format
- reorganized library to make requiring it easier
Diffstat (limited to 'doc/macros.md')
| -rw-r--r-- | doc/macros.md | 588 |
1 files changed, 588 insertions, 0 deletions
diff --git a/doc/macros.md b/doc/macros.md new file mode 100644 index 0000000..e9cc517 --- /dev/null +++ b/doc/macros.md @@ -0,0 +1,588 @@ +# Macros.fnl (0.3.0) +Macros for Cljlib that implement various facilities from Clojure. + +**Table of contents** + +- [`fn*`](#fn*) +- [`try`](#try) +- [`def`](#def) +- [`defonce`](#defonce) +- [`defmulti`](#defmulti) +- [`defmethod`](#defmethod) +- [`into`](#into) +- [`empty`](#empty) +- [`when-meta`](#when-meta) +- [`with-meta`](#with-meta) +- [`meta`](#meta) +- [`if-let`](#if-let) +- [`when-let`](#when-let) +- [`if-some`](#if-some) +- [`when-some`](#when-some) +- [`deep-tostring`](#deep-tostring) + +## `fn*` +Function signature: + +``` +(fn* name docstring? ([arglist*] body)*) +``` + +Create (anonymous) function of fixed arity. +Supports multiple arities by defining bodies as lists. + +### Examples +Named function of fixed arity 2: + +``` fennel +(fn* f [a b] (+ a b)) +``` + +Function of fixed arities 1 and 2: + +``` fennel +(fn* ([x] x) + ([x y] (+ x y))) +``` + +Named function of 2 arities, one of which accepts 0 arguments, and the +other one or more arguments: + +``` fennel +(fn* f + ([] nil) + ([x & xs] + (print x) + (f (unpack xs)))) +``` + +Note, that this function is recursive, and calls itself with less and +less amount of arguments until there's no arguments, and terminates +when the zero-arity body is called. + +Named functions accept additional documentation string before the +argument list: + +``` fennel +(fn* cube + "raise `x` to power of 3" + [x] + (^ x 3)) + +(fn* greet + "greet a `person`, optionally specifying default `greeting`." + ([person] (print (.. "Hello, " person "!"))) + ([greeting person] (print (.. greeting ", " person "!")))) +``` + +Argument lists follow the same destruction rules as per `let`. +Variadic arguments with `...` are not supported use `& rest` instead. +Note that only one arity with `&` is supported. + +##### Namespaces +If function name contains namespace part, defines local variable +without namespace part, then creates function with this name, sets +this function to the namespace, and returns it. + +This roughly means, that instead of writing this: + +``` fennel +(local ns {}) + +(fn f [x] ;; we have to define `f` without `ns` + (if (> x 0) (f (- x 1)))) ;; because we're going to use it in `g` + +(set ns.f f) + +(fn ns.g [x] (f (* x 100))) ;; `g` can be defined as `ns.g` as it is only exported + +ns +``` + +It is possible to write: + +``` fennel +(local ns {}) + +(fn* ns.f [x] + (if (> x 0) (f (- x 1)))) + +(fn* ns.g [x] (f (* x 100))) ;; we can use `f` here no problem + +ns +``` + +It is still possible to call `f` and `g` in current scope without `ns` +part, so functions can be reused inside the module, and `ns` will hold +both functions, so it can be exported from the module. + +Note that `fn` will not create the `ns` for you, hence this is just a +syntax sugar. Functions deeply nested in namespaces require exising +namespace tables: + +``` fennel +(local ns {:strings {} + :tables {}}) + +(fn* ns.strings.join + ([s1 s2] (.. s1 s2)) + ([s1 s2 & strings] + (join (join s1 s2) (unpack strings)))) ;; call `join` resolves to ns.strings.join + +(fn* ns.tables.join + ([t1 t2] + (let [res []] + (each [_ v (ipairs t1)] (table.insert res v)) + (each [_ v (ipairs t2)] (table.insert res v)) + res)) + ([t1 t2 & tables] + (join (join t1 t2) (unpack tables)))) ;; call to `join` resolves to ns.tables.join +``` + +Note that this creates a collision and local `join` overrides `join` +from `ns.strings`, so the latter must be fully qualified +`ns.strings.join` when called outside of the function: + +``` fennel +(ns.strings.join "a" "b" "c") +;; => abc +(join ["a"] ["b"] ["c"] ["d" "e"]) +;; => ["a" "b" "c" "d" "e"] +(join "a" "b" "c") +;; {} +``` + +## `try` +Function signature: + +``` +(try body* catch-clause* finally-clause?) +``` + +General purpose try/catch/finally macro. +Wraps its body in `pcall` and checks the return value with `match` +macro. + +Catch clause is written either as (catch symbol body*), thus acting as +catch-all, or (catch value body*) for catching specific errors. It is +possible to have several `catch` clauses. If no `catch` clauses +specified, an implicit catch-all clause is created. + +Finally clause is optional, and written as (finally body*). If +present, it must be the last clause in the `try` form, and the only +`finally` clause. Note that `finally` clause is for side effects +only, and runs either after succesful run of `try` body, or after any +`catch` clause body, before returning the result. If no `catch` +clause is provided `finally` runs in implicit catch-all clause, and +trows error to upper scope using `error` function. + +To throw error from `try` to catch it with `catch` clause use `error` +or `assert` functions. + +### Examples +Catch all errors, ignore those and return fallback value: + +``` fennel +(fn add [x y] + (try + (+ x y) + (catch _ 0))) + +(add nil 1) ;; => 0 +``` + +Catch error and do cleanup: + +``` fennel +>> (let [tbl []] + (try + (table.insert tbl "a") + (table.insert tbl "b" "c") + (catch _ + (each [k _ (pairs tbl)] + (tset tbl k nil)))) + tbl) +{} +``` + +Always run some side effect action: + +``` fennel +>> (local res (try 10 (finally (print "side-effect!"))) +side-effect! +nil +>> rese0 +>> (local res (try (error 10) (catch 10 nil) (finally (print "side-effect!"))) +side-effect! +nil +>> res +nil +``` + + +## `def` +Function signature: + +``` +(def attr-map? name expr) +``` + +Wrapper around `local` which can +declare variables inside namespace, and as local at the same time +similarly to [`fn*`](#fn*): + +``` fennel +(def ns {}) +(def a 10) ;; binds `a` to `10` + +(def ns.b 20) ;; binds `ns.b` and `b` to `20` +``` + +`a` is a `local`, and both `ns.b` and `b` refer to the same value. + +Additionally metadata can be attached to values, by providing +attribute map or keyword as first parameter. Only one keyword is +supported, which is `:mutable`, which allows mutating variable with +`set` later on: + +``` fennel +;; Bad, will override existing documentation for 299792458 (if any) +(def {:doc "speed of light in m/s"} c 299792458) +(set c 0) ;; => error, can't mutate `c` + +(def :mutable address "Lua St.") ;; same as (def {:mutable true} address "Lua St.") +(set address "Lisp St.") ;; can mutate `address` +``` + +However, attaching documentation metadata to anything other than +tables and functions considered bad practice, due to how Lua +works. More info can be found in [`with-meta`](#with-meta) +description. + +## `defonce` +Function signature: + +``` +(defonce attr-map? name expr) +``` + +Works the same as [`def`](#def), but ensures that later `defonce` +calls will not override existing bindings: + +``` fennel +(defonce a 10) +(defonce a 20) +(print a) ;; => prints 10 +``` + +## `defmulti` +Function signature: + +``` +(defmulti name docstring? dispatch-fn attr-map?) +``` + +Create multifunction with +runtime dispatching based on results from `dispatch-fn`. Returns an +empty table with `__call` metamethod, that calls `dispatch-fn` on its +arguments. Amount of arguments passed, should be the same as accepted +by `dispatch-fn`. Looks for multimethod based on result from +`dispatch-fn`. + +By default, multifunction has no multimethods, see +[`multimethod`](#multimethod) on how to add one. + +## `defmethod` +Function signature: + +``` +(defmethod multifn dispatch-val fnspec) +``` + +Attach new method to multi-function dispatch value. accepts the `multi-fn` +as its first argument, the dispatch value as second, and function tail +starting from argument list, followed by function body as in +[`fn*`](#fn). + +### Examples +Here are some examples how multimethods can be used. + +#### Factorial example +Key idea here is that multimethods can call itself with different +values, and will dispatch correctly. Here, `fac` recursively calls +itself with less and less number until it reaches `0` and dispatches +to another multimethod: + +``` fennel +(defmulti fac (fn [x] x)) + +(defmethod fac 0 [_] 1) +(defmethod fac :default [x] (* x (fac (- x 1)))) + +(fac 4) ;; => 24 +``` + +`:default` is a special method which gets called when no other methods +were found for given dispatch value. + +#### Multi-arity dispatching +Multi-arity function tails are also supported: + +``` fennel +(defmulti foo (fn* ([x] [x]) ([x y] [x y]))) + +(defmethod foo [10] [_] (print "I've knew I'll get 10")) +(defmethod foo [10 20] [_ _] (print "I've knew I'll get both 10 and 20")) +(defmethod foo :default ([x] (print (.. "Umm, got" x))) + ([x y] (print (.. "Umm, got both " x " and " y)))) +``` + +Calling `(foo 10)` will print `"I've knew I'll get 10"`, and calling +`(foo 10 20)` will print `"I've knew I'll get both 10 and 20"`. +However, calling `foo` with any other numbers will default either to +`"Umm, got x"` message, when called with single value, and `"Umm, got +both x and y"` when calling with two values. + +#### Dispatching on object's type +We can dispatch based on types the same way we dispatch on values. +For example, here's a naive conversion from Fennel's notation for +tables to Lua's one: + +``` fennel +(defmulti to-lua-str (fn [x] (type x))) + +(defmethod to-lua-str :number [x] (tostring x)) +(defmethod to-lua-str :table [x] (let [res []] + (each [k v (pairs x)] + (table.insert res (.. "[" (to-lua-str k) "] = " (to-lua-str v)))) + (.. "{" (table.concat res ", ") "}"))) +(defmethod to-lua-str :string [x] (.. "\"" x "\"")) +(defmethod to-lua-str :default [x] (tostring x)) +``` + +And if we call it on some table, we'll get a valid Lua table: + +``` fennel +(print (to-lua-str {:a {:b 10}})) +;; prints {["a"] = {["b"] = 10}} + +(print (to-lua-str [:a :b :c [:d {:e :f}]])) +;; prints {[1] = "a", [2] = "b", [3] = "c", [4] = {[1] = "d", [2] = {["e"] = "f"}}} +``` + +Which we can then reformat as we want and use in Lua if we want. + +## `into` +Function signature: + +``` +(into to from) +``` + +Transform one table into another. Mutates first table. + +Transformation happens in runtime, but type deduction happens in +compile time if possible. This means, that if literal values passed +to `into` this will have different effects for associative tables and +vectors: + +``` fennel +(into [1 2 3] [4 5 6]) ;; => [1 2 3 4 5 6] +(into {:a 1 :c 2} {:a 0 :b 1}) ;; => {:a 0 :b 1 :c 2} +``` + +Conversion between different table types is also supported: + +``` fennel +(into [] {:a 1 :b 2 :c 3}) ;; => [[:a 1] [:b 2] [:c 3]] +(into {} [[:a 1] [:b 2]]) ;; => {:a 1 :b 2} +``` + +Same rules apply to runtime detection of table type, except that this +will not work for empty tables: + +``` fennel +(local empty-table {}) +(into empty-table {:a 1 :b 2}) ;; => [[:a 1] [:b 2]] +``` fennel + +If table is empty, `into` defaults to sequential table, because it +allows safe conversion from both sequential and associative tables. + +Type for non empty tables hidden in variables can be deduced at +runtime, and this works as expected: + +``` fennel +(local t1 [1 2 3]) +(local t2 {:a 10 :c 3}) +(into t1 {:a 1 :b 2}) ;; => [1 2 3 [:a 1] [:b 2]] +(into t2 {:a 1 :b 2}) ;; => {:a 1 :b 2 :c 3} +``` + +`cljlib.fnl` module provides two additional functions `vector` and +`hash-map`, that can create empty tables, which can be distinguished +at runtime: + +``` fennel +(into (vector) {:a 1 :b 2}) ;; => [[:a 1] [:b 2]] +(into (hash-map) [[:a 1 :b 2]]) ;; => {:a 1 :b 2} +``` + +## `empty` +Function signature: + +``` +(empty x) +``` + +Return empty table of the same kind as input table `x`, with +additional metadata indicating its type. + +### Example +Creating a generic `map` function, that will work on any table type, +and return result of the same type: + +``` fennel +(fn map [f tbl] + (let [res []] + (each [_ v (ipairs (into [] tbl))] + (table.insert res (f v))) + (into (empty tbl) res))) + +(map (fn [[k v]] [(string.upper k) v]) {:a 1 :b 2 :c 3}) +;; => {:A 1 :B 2 :C 3} +(map #(* $ $) [1 2 3 4]) +;; [1 4 9 16] +``` +See [`into`](#into) for more info on how conversion is done. + +## `when-meta` +Function signature: + +``` +(when-meta [& body]) +``` + +Wrapper that compiles away if metadata support was not enabled. What +this effectively means, is that everything that is wrapped with this +macro will disappear from the resulting Lua code if metadata is not +enabled when compiling with `fennel --compile` without `--metadata` +switch. + +## `with-meta` +Function signature: + +``` +(with-meta value meta) +``` + +Attach metadata to a value. When metadata feature is not enabled, +returns the value without additional metadata. + +``` fennel +>> (local foo (with-meta (fn [...] (let [[x y z] [...]] (+ x y z))) + {:fnl/arglist ["x" "y" "z" "..."] + :fnl/docstring "sum first three values"})) +>> (doc foo) +(foo x y z ...) + sum first three values +``` + +## `meta` +Function signature: + +``` +(meta value) +``` + +Get `value` metadata. If value has no metadata, or metadata +feature is not enabled returns `nil`. + +### Example + +``` fennel +>> (meta (with-meta {} {:meta "data"})) +;; => {:meta "data"} +``` + +### Note +There are several important gotchas about using metadata. + +First, note that this works only when used with Fennel, and only when +`(require fennel)` works. For compiled Lua library this feature is +turned off. + +Second, try to avoid using metadata with anything else than tables and +functions. When storing function or table as a key into metatable, +its address is used, while when storing string of number, the value is +used. This, for example, may cause documentation collision, when +you've set some variable holding a number value to have certain +docstring, and later you've defined another variable with the same +value, but different docstring. While this isn't a major breakage, it +may confuse if someone will explore your code in the REPL with `doc`. + +Lastly, note that prior to Fennel 0.7.1 `import-macros` wasn't +respecting `--metadata` switch. So if you're using Fennel < 0.7.1 +this stuff will only work if you use `require-macros` instead of +`import-macros`. + +## `if-let` +Function signature: + +``` +(if-let [binding test] then-branch else-branch) +``` + +If test is logical true, +evaluates `then-branch` with binding-form bound to the value of test, +if not, yields `else-branch`. + +## `when-let` +Function signature: + +``` +(when-let [binding test] & body) +``` + +If test is logical true, +evaluates `body` in implicit `do`. + +## `if-some` +Function signature: + +``` +(if-some [binding test] then-branch else-branch) +``` + +If test is non-`nil`, evaluates +`then-branch` with binding-form bound to the value of test, if not, +yields `else-branch`. + +## `when-some` +Function signature: + +``` +(when-some [binding test] & body) +``` + +If test is non-`nil`, +evaluates `body` in implicit `do`. + +## `deep-tostring` +Function signature: + +``` +(deep-tostring data key?) +``` + +**Undocumented** + + +--- + +Copyright (C) 2020 Andrey Orst + +License: [MIT](https://gitlab.com/andreyorst/fennel-cljlib/-/raw/master/LICENSE) + + +<!-- Generated with Fenneldoc 0.0.7 + https://gitlab.com/andreyorst/fenneldoc --> |