diff options
| -rw-r--r-- | cljlib-macros.fnl | 647 | ||||
| -rw-r--r-- | cljlib.fnl | 12 | ||||
| -rw-r--r-- | doc/cljlib-macros.md | 631 | ||||
| -rw-r--r-- | doc/cljlib.md | 8 | ||||
| -rw-r--r-- | tests/core.fnl | 3 | ||||
| -rw-r--r-- | tests/fn.fnl | 10 | ||||
| -rw-r--r-- | tests/macros.fnl | 10 |
7 files changed, 949 insertions, 372 deletions
diff --git a/cljlib-macros.fnl b/cljlib-macros.fnl index e62c185..7ff93bd 100644 --- a/cljlib-macros.fnl +++ b/cljlib-macros.fnl @@ -1,15 +1,67 @@ +(local fennel (require :fennel)) (local meta-enabled (pcall _SCOPE.specials.doc (list (sym :doc) (sym :doc)) _SCOPE _CHUNK)) -(fn eq-fn [] - "Returns recursive equality function. +;; helper functions + +(fn first [tbl] + (. tbl 1)) + +(fn rest [tbl] + [((or table.unpack _G.unpack) tbl 2)]) + +(fn string? [x] + (= (type x) :string)) + +(fn multisym->sym [s] + ;; Strip multisym part from symbol and return new symbol and + ;; indication that sym was transformed. Non-multisym symbols returned as + ;; is. + ;; + ;; ``` fennel + ;; (multisym->sym a.b) ;; => (a true) + ;; (multisym->sym a.b.c) ;; => (c true) + ;; (multisym->sym a) ;; => (a false) + ;; ``` + (values (sym (string.match (tostring s) "[^.]+$")) + (multi-sym? s))) + +(fn contains? [tbl x] + ;; Checks if `x` is stored in `tbl` in linear time. + (var res false) + (each [i v (ipairs tbl)] + (if (= v x) + (do (set res i) + (lua :break)))) + res) + +(fn check-two-binding-vec [bindings] + ;; Test if `bindings` is a `sequence` that holds two forms, first of + ;; which is a `sym`, `table` or `sequence`. + (and (assert-compile (sequence? bindings) + "expected binding table" []) + (assert-compile (= (length bindings) 2) + "expected exactly two forms in binding vector." bindings) + (assert-compile (or (sym? (first bindings)) + (sequence? (first bindings)) + (table? (first bindings))) + "expected symbol, sequence or table as binding." bindings))) + +(fn attach-meta [value meta] + (each [k v (pairs meta)] + (fennel.metadata:set value k v))) -This function is able to compare tables of any depth, even if one of -the tables uses tables as keys." +;; Runtime function builders + +(fn eq-fn [] + ;; Returns recursive equality function. + ;; + ;; This function is able to compare tables of any depth, even if one of + ;; the tables uses tables as keys. `(fn eq# [left# right#] (if (and (= (type left#) :table) (= (type right#) :table)) (let [oldmeta# (getmetatable right#)] ;; In case if we'll get something like - ;; (eq {[1 2 3] {:a [1 2 3]}} {[1 2 3] {:a [1 2 3]}}) + ;; `(eq {[1 2 3] {:a [1 2 3]}} {[1 2 3] {:a [1 2 3]}})` ;; we have to do even deeper search (setmetatable right# {:__index (fn [tbl# key#] (var res# nil) @@ -32,14 +84,14 @@ the tables uses tables as keys." (= left# right#)))) (fn seq-fn [] - "Returns function that transforms tables and strings into sequences. - -Sequential tables `[1 2 3 4]' are shallowly copied. - -Assocative tables `{:a 1 :b 2}' are transformed into `[[:a 1] [:b 2]]' -with nondeterministic order. - -Strings are transformed into a sequence of letters." + ;; Returns function that transforms tables and strings into sequences. + ;; + ;; Sequential tables `[1 2 3 4]` are shallowly copied. + ;; + ;; Associative tables `{:a 1 :b 2}` are transformed into `[[:a 1] [:b 2]]` + ;; with non deterministic order. + ;; + ;; Strings are transformed into a sequence of letters. `(fn [col#] (let [type# (type col#) res# (setmetatable {} {:cljlib/table-type :seq}) @@ -62,25 +114,102 @@ Strings are transformed into a sequence of letters." (= type# :nil) nil (error "expected table, string or nil" 2))))) -(fn with-meta [val meta] - (if (not meta-enabled) val - `(let [val# ,val +(fn table-type-fn [] + `(fn [tbl#] + (let [t# (type tbl#)] + (if (= t# :table) + (let [meta# (getmetatable tbl#) + table-type# (and meta# (. meta# :cljlib/table-type))] + (if table-type# table-type# + (let [(k# _#) (next tbl#)] + (if (and (= (type k#) :number) (= k# 1)) :seq + (= k# nil) :empty + :table)))) + (= t# :nil) :nil + (= t# :string) :string + :else)))) + +;; Metadata + +(fn when-meta [...] + "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." + (when meta-enabled + `(do ,...))) + +(attach-meta when-meta {:fnl/arglist ["[& body]"]}) + +(fn 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`." + (when-meta + `(let [(res# fennel#) (pcall require :fennel)] + (if res# (. fennel#.metadata ,value))))) + +(fn 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 +```" + (if (not meta-enabled) value + `(let [value# ,value (res# fennel#) (pcall require :fennel)] (if res# (each [k# v# (pairs ,meta)] - (fennel#.metadata:set val# k# v#))) - val#))) + (fennel#.metadata:set value# k# v#))) + value#))) + +;; fn* (fn gen-arglist-doc [args] + ;; Construct vector of arguments represented as strings from AST. (if (list? (. args 1)) (let [arglist [] - newline? (if (> (length args) 1) "\n (" "(")] + opener (if (> (length args) 1) "\n (" "(")] (each [i v (ipairs args)] (let [arglist-doc (gen-arglist-doc v)] (when (next arglist-doc) (table.insert arglist - (.. newline? (table.concat arglist-doc " ") ")"))))) + (.. opener (table.concat arglist-doc " ") ")"))))) arglist) (sequence? (. args 1)) @@ -101,12 +230,9 @@ Strings are transformed into a sequence of letters." (values (sym (string.gsub (tostring s) ".*[.]" "")) true) (values s false))) -(fn string? [x] - (= (type x) "string")) - (fn has-amp? [args] - ;; Check if arglist has `&' and return its position of `false'. - ;; Performs additional checks for `&' and `...' usage in arglist. + ;; Check if arglist has `&` and return its position of `false`. Performs + ;; additional checks for `&` and `...` usage in arglist. (var res false) (each [i s (ipairs args)] (if (= (tostring s) "&") @@ -123,7 +249,7 @@ Strings are transformed into a sequence of letters." ;; ;; - the length of arglist; ;; - the body of the function we generate; - ;; - position of `&' in the arglist if any. + ;; - position of `&` in the arglist if any. (assert-compile (sequence? args) "fn*: expected parameters table. * Try adding function parameters as a list of identifiers in brackets." args) @@ -131,15 +257,16 @@ Strings are transformed into a sequence of letters." (list 'let [args ['...]] (list 'do ((or table.unpack _G.unpack) body))) (has-amp? args))) -(fn contains? [tbl x] - (var res false) - (each [i v (ipairs tbl)] - (if (= v x) - (do (set res i) - (lua :break)))) - res) - (fn grows-by-one-or-equal? [tbl] + ;; Checks if table consists of integers that grow by one or equal to + ;; eachother when sorted. Used for checking if we supplied all arities + ;; for dispatching, and there's no need in the error handling. + ;; + ;; ``` fennel + ;; (grows-by-one-or-equal? [1 3 2]) => true, because [1 2 3] + ;; (grows-by-one-or-equal? [1 4 2]) => true, because 3 is missing + ;; (grows-by-one-or-equal? [1 3 2 3]) => true, because equal values are allowed. + ;; ``` (let [t []] (each [_ v (ipairs tbl)] (table.insert t v)) (table.sort t) @@ -153,24 +280,23 @@ Strings are transformed into a sequence of letters." prev)) (fn arity-dispatcher [len fixed body& name] - ;; Forms an `if' expression with all fixed arities first, then `&' - ;; arity, if present, and default error message as last arity. + ;; Forms an `if` expression with all fixed arities first, then `&` arity, + ;; if present, and default error message as last arity. ;; - ;; `len' is a symbol, that represents the length of the current argument + ;; `len` is a symbol, that represents the length of the current argument ;; list, and is computed at runtime. ;; - ;; `fixed' is a table of arities with fixed amount of arguments. - ;; These are put in this `if' as: `(= len fixed-len)', where - ;; `fixed-len' is the length of current arity arglist, computed with - ;; `gen-arity'. + ;; `fixed` is a table of arities with fixed amount of arguments. These + ;; are put in this `if` as: `(= len fixed-len)`, where `fixed-len` is the + ;; length of current arity arglist, computed with `gen-arity`. ;; - ;; `body&' stores size of fixed part of arglist, that is, everything - ;; up until `&', and the body itself. When `body&' provided, the - ;; `(>= len more-len)' is added to the resulting `if' expression. + ;; `body&` stores size of fixed part of arglist, that is, everything up + ;; until `&`, and the body itself. When `body&` provided, the `(>= len + ;; more-len)` is added to the resulting `if` expression. ;; - ;; Lastly the catchall branch is added to `if' expression, which - ;; ensures that only valid amount of arguments were passed to - ;; function, which are defined by previous branches. + ;; Lastly the catchall branch is added to `if` expression, which ensures + ;; that only valid amount of arguments were passed to function, which are + ;; defined by previous branches. (let [bodies '(if) lengths []] (var max nil) @@ -232,63 +358,126 @@ Strings are transformed into a sequence of letters." "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))) + ([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 the -zero-arity body is called. +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\" + \"raise `x` to power of 3\" [x] (^ x 3)) (fn* greet - \"greet a `person', optionally specifying default `greeting'.\" + \"greet a `person`, optionally specifying default `greeting`.\" ([person] (print (.. \"Hello, \" person \"!\"))) ([greeting person] (print (.. greeting \", \" person \"!\")))) +``` -Argument lists follow the same destruction rules as in `let'. -Variadic arguments with `...' are not supported. +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: +this function to the namespace, and returns it. -(local namespace {}) -(fn f [x] - (if (> x 0) (f (- x 1)))) -(set namespace.f f) -(fn g [x] (f (* x 100))) -(set namespace.g g) +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: -(local namespace {}) -(fn* namespace.f [x] +``` fennel +(local ns {}) + +(fn* ns.f [x] (if (> x 0) (f (- x 1)))) -(fn* namespace.g [x] (f (* x 100))) -Note that it is still possible to call `f' and `g' in current scope -without namespace part. `Namespace' will hold both functions as `f' -and `g' respectively." +(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\") +;; {} +```" (assert-compile (not (string? name)) "fn* expects symbol, vector, or list as first argument" name) (let [docstring (if (string? doc?) doc? nil) (name-wo-namespace namespaced?) (multisym->sym name) @@ -310,46 +499,20 @@ and `g' respectively." (do (fn ,name-wo-namespace [...] ,docstring ,body) (set ,name ,name-wo-namespace) - ,(with-meta name-wo-namespace `{:fnl/arglist ,arglist-doc :fnl/docstring ,docstring}))) - `(local ,name ,(with-meta `(fn ,name [...] ,docstring ,body) `{:fnl/arglist ,arglist-doc :fnl/docstring ,docstring}))) - (with-meta `(fn [...] ,docstring ,body) `{:fnl/arglist ,arglist-doc :fnl/docstring ,docstring})))) - -(fn fn+ [name doc? args ...] - "Create (anonymous) function. -Works the same as plain `fn' except supports automatic declaration of -namespaced functions. See `fn*' for more info." - (assert-compile (not (string? name)) "fn* expects symbol, vector, or list as first argument" name) - (let [docstring (if (string? doc?) doc? nil) - (name-wo-namespace namespaced?) (multisym->sym name) - arg-list (if (sym? name-wo-namespace) - (if (string? doc?) args doc?) - name-wo-namespace) - arglist-doc (gen-arglist-doc arg-list) - body (if (sym? name) - (if (string? doc?) - [doc? ...] - [args ...]) - [doc? args ...])] - (if (sym? name-wo-namespace) - (if namespaced? - `(local ,name-wo-namespace - (do - (fn ,name-wo-namespace ,arg-list ,((or table.unpack _G.unpack) body)) - (set ,name ,name-wo-namespace) - ,(with-meta name-wo-namespace `{:fnl/arglist ,arglist-doc :fnl/docstring ,docstring}))) - `(local ,name ,(with-meta `(fn ,name ,arg-list ,((or table.unpack _G.unpack) body)) `{:fnl/arglist ,arglist-doc :fnl/docstring ,docstring}))) - (with-meta `(fn ,arg-list ,((or table.unpack _G.unpack) body)) `{:fnl/arglist ,arglist-doc :fnl/docstring ,docstring})))) + ,(with-meta name-wo-namespace `{:fnl/arglist ,arglist-doc}))) + `(local ,name ,(with-meta `(fn ,name [...] ,docstring ,body) `{:fnl/arglist ,arglist-doc}))) + (with-meta `(fn [...] ,docstring ,body) `{:fnl/arglist ,arglist-doc})))) -(fn check-bindings [bindings] - (and (assert-compile (sequence? bindings) "expected binding table" []) - (assert-compile (= (length bindings) 2) "expected exactly two forms in binding vector." bindings))) +(attach-meta fn* {:fnl/arglist ["name docstring? [arglist*] body*" + "name docstring ([arglist*] body)*"]}) +;; let variants (fn if-let [...] (let [[bindings then else] (match (select :# ...) 2 [...] 3 [...] _ (error "wrong argument amount for if-some" 2))] - (check-bindings bindings) + (check-two-binding-vec bindings) (let [[form test] bindings] `(let [tmp# ,test] (if tmp# @@ -357,22 +520,33 @@ namespaced functions. See `fn*' for more info." ,then) ,else))))) +(attach-meta if-let {:fnl/arglist ["[binding test]" "then-branch" "else-branch"] + :fnl/docstring "If test is logical true, +evaluates `then-branch` with binding-form bound to the value of test, +if not, yields `else-branch`."}) + + (fn when-let [...] (let [[bindings & body] (if (> (select :# ...) 0) [...] (error "wrong argument amount for when-let" 2))] - (check-bindings bindings) + (check-two-binding-vec bindings) (let [[form test] bindings] `(let [tmp# ,test] (if tmp# (let [,form tmp#] ,((or table.unpack _G.unpack) body))))))) +(attach-meta when-let {:fnl/arglist ["[binding test]" "& body"] + :fnl/docstring "If test is logical true, +evaluates `body` in implicit `do`."}) + + (fn if-some [...] (let [[bindings then else] (match (select :# ...) 2 [...] 3 [...] _ (error "wrong argument amount for if-some" 2))] - (check-bindings bindings) + (check-two-binding-vec bindings) (let [[form test] bindings] `(let [tmp# ,test] (if (= tmp# nil) @@ -380,10 +554,16 @@ namespaced functions. See `fn*' for more info." (let [,form tmp#] ,then)))))) +(attach-meta if-some {:fnl/arglist ["[binding test]" "then-branch" "else-branch"] + :fnl/docstring "If test is non-`nil`, evaluates +`then-branch` with binding-form bound to the value of test, if not, +yields `else-branch`."}) + + (fn when-some [...] (let [[bindings & body] (if (> (select :# ...) 0) [...] (error "wrong argument amount for when-some" 2))] - (check-bindings bindings) + (check-two-binding-vec bindings) (let [[form test] bindings] `(let [tmp# ,test] (if (= tmp# nil) @@ -391,34 +571,67 @@ namespaced functions. See `fn*' for more info." (let [,form tmp#] ,((or table.unpack _G.unpack) body))))))) +(attach-meta when-some {:fnl/arglist ["[binding test]" "& body"] + :fnl/docstring "If test is non-`nil`, +evaluates `body` in implicit `do`."}) + +;;;;;;;;;;;;;;;;;; into ;;;;;;;;;;;;;;;;;; (fn table-type [tbl] (if (sequence? tbl) :seq (table? tbl) :table :else)) -(fn table-type-fn [] - `(fn [tbl#] - (let [t# (type tbl#)] - (if (= t# :table) - (let [meta# (getmetatable tbl#) - table-type# (and meta# (. meta# :cljlib/table-type))] - (if table-type# table-type# - (let [(k# _#) (next tbl#)] - (if (and (= (type k#) :number) (= k# 1)) :seq - (= k# nil) :empty - :table)))) - (= t# :nil) :nil - (= t# :string) :string - :else)))) - -(fn empty [tbl] - (let [table-type (table-type tbl)] - (if (= table-type :seq) `(setmetatable {} {:cljlib/table-type :seq}) - (= table-type :table) `(setmetatable {} {:cljlib/table-type :table}) - `(setmetatable {} {:cljlib/table-type (,(table-type-fn) ,tbl)})))) - (fn 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} +```" + (assert-compile (and to from) "into: expected two arguments") (let [to-type (table-type to) from-type (table-type from)] (if (and (= to-type :seq) (= from-type :seq)) @@ -503,23 +716,35 @@ namespaced functions. See `fn*' for more info." :empty :seq :table :table)})))))) -(fn first [tbl] - (. tbl 1)) +;; empty -(fn rest [tbl] - [((or table.unpack _G.unpack) tbl 2)]) +(fn empty [x] + "Return empty table of the same kind as input table `x`, with +additional metadata indicating its type. -(fn string? [x] - (= (type x) :string)) +# Example +Creating a generic `map` function, that will work on any table type, +and return result of the same type: -(fn when-meta [...] - (when meta-enabled - `(do ,...))) +``` fennel +(fn map [f tbl] + (let [res []] + (each [_ v (ipairs (into [] tbl))] + (table.insert res (f v))) + (into (empty tbl) res))) -(fn meta [v] - (when-meta - `(let [(res# fennel#) (pcall require :fennel)] - (if res# (. fennel#.metadata ,v))))) +(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." + (match (table-type x) + :seq `(setmetatable {} {:cljlib/table-type :seq}) + :table `(setmetatable {} {:cljlib/table-type :table}) + _ `(setmetatable {} {:cljlib/table-type (,(table-type-fn) ,x)}))) + +;; multimethods (fn seq->table [seq] (let [tbl {}] @@ -570,6 +795,18 @@ namespaced functions. See `fn*' for more info." (lua :break))) res#))})}))))))) +(attach-meta defmulti {:fnl/arglist [:name :docstring? :dispatch-fn :attr-map?] + :fnl/docstring "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."}) + + (fn defmethod [multifn dispatch-val ...] (when (= (select :# ...) 0) (error "wrong argument amount for defmethod")) `(let [multifn# ,multifn] @@ -579,6 +816,82 @@ namespaced functions. See `fn*' for more info." f#)) multifn#)) +(attach-meta defmethod {:fnl/arglist [:multifn :dispatch-val :fnspec] + :fnl/docstring "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."}) + +;; def and defonce + (fn def [...] (let [[attr-map name expr] (match (select :# ...) 2 [{} ...] @@ -590,13 +903,47 @@ namespaced functions. See `fn*' for more info." (s multi) (multisym->sym name) docstring (or (. attr-map :doc) (. attr-map :fnl/docstring)) - f (if (. attr-map :dynamic) 'var 'local)] + f (if (. attr-map :mutable) 'var 'local)] (if multi `(,f ,s (do (,f ,s ,expr) (set ,name ,s) ,(with-meta s {:fnl/docstring docstring}))) `(,f ,name ,(with-meta expr {:fnl/docstring docstring}))))) +(attach-meta def {:fnl/arglist [:attr-map? :name :expr] + :fnl/docstring "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."}) + + (fn defonce [...] (let [[attr-map name expr] (match (select :# ...) 2 [{} ...] @@ -606,8 +953,17 @@ namespaced functions. See `fn*' for more info." nil (def attr-map name expr)))) +(attach-meta defonce {:fnl/arglist [:attr-map? :name :expr] + :fnl/docstring "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 +```"}) + {: fn* - : fn+ : if-let : when-let : if-some @@ -620,7 +976,14 @@ namespaced functions. See `fn*' for more info." : defmulti : defmethod : def - : defonce} + : defonce + :_VERSION #"0.2.0" + :_LICENSE #"[MIT](https://gitlab.com/andreyorst/fennel-cljlib/-/raw/master/LICENSE)" + :_COPYRIGHT #"Copyright (C) 2020 Andrey Orst" + :_DESCRIPTION #"Macros for Cljlib that implement various facilities from Clojure."} ;; LocalWords: arglist fn runtime arities arity multi destructuring -;; LocalWords: docstring Variadic LocalWords +;; LocalWords: docstring Variadic LocalWords multisym sym tbl eq Lua +;; LocalWords: defonce metadata metatable fac defmulti Umm defmethod +;; LocalWords: multimethods multimethod multifn REPL fnl AST Lua's +;; LocalWords: lua tostring str concat namespace ns Cljlib Clojure @@ -1,4 +1,4 @@ -(local core {:_VERSION "0.1.0" +(local core {:_VERSION "0.2.0" :_LICENSE "[MIT](https://gitlab.com/andreyorst/fennel-cljlib/-/raw/master/LICENSE)" :_COPYRIGHT "Copyright (C) 2020 Andrey Orst" :_DESCRIPTION "Fennel-cljlib - functions from Clojure's core.clj implemented on top @@ -19,7 +19,7 @@ This example is mapping an anonymous `function` over a table, producing new table and concatenating it with `\" \"`. However this library also provides Fennel-specific set of -[macros](./cljlib-macros.md), that provides additional facilites like +[macros](./cljlib-macros.md), that provides additional facilities like `fn*` or `defmulti` which extend the language allowing writing code that looks and works mostly like Clojure. @@ -33,7 +33,7 @@ brackets). Functions, which signatures look like `(foo ([x]) ([x y]) ([x y & zs]))`, it is a multi-arity function, which accepts either one, two, or three-or-more arguments. Each `([...])` represents different body -of a function which is choosed by checking amount of arguments passed +of a function which is chosen by checking amount of arguments passed to the function. See [Clojure's doc section on multi-arity functions](https://clojure.org/guides/learn/functions#_multi_arity_functions)."}) @@ -58,7 +58,7 @@ Sets additional metadata for function [`vector?`](#vector?) to work. (fn* core.apply "Apply `f` to the argument list formed by prepending intervening -arguments to `args`, adn `f` must support variadic amount of +arguments to `args`, and `f` must support variadic amount of arguments. # Examples @@ -929,3 +929,7 @@ use." res)))))) core + +;; LocalWords: cljlib Clojure's clj lua PUC mapv concat Clojure fn zs +;; LocalWords: defmulti multi arity eq metadata prepending variadic +;; LocalWords: args tbl LocalWords memoized referentially diff --git a/doc/cljlib-macros.md b/doc/cljlib-macros.md index d99dad6..5916521 100644 --- a/doc/cljlib-macros.md +++ b/doc/cljlib-macros.md @@ -1,288 +1,509 @@ -# Cljlib-macros.fnl -Macro module for Fennel Cljlib. +# Cljlib-macros.fnl (0.1.0) +Macros for Cljlib that implement various facilities from Clojure. + +**Table of contents** + +- [`def`](#def) +- [`defmethod`](#defmethod) +- [`defmulti`](#defmulti) +- [`defonce`](#defonce) +- [`empty`](#empty) +- [`fn*`](#fn*) +- [`if-let`](#if-let) +- [`if-some`](#if-some) +- [`into`](#into) +- [`meta`](#meta) +- [`when-let`](#when-let) +- [`when-meta`](#when-meta) +- [`when-some`](#when-some) +- [`with-meta`](#with-meta) + +## `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. + +## `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))) -## Metadata macros -Metadata in Fennel is a pretty tough subject, as there's no such thing as metadata in Lua. -Therefore, the metadata usage in Fennel is more limited compared to Clojure. -This library provides some facilities for metadata management, which are experimental and should be used with care. +(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)) +``` -There are several important gotchas about using metadata. +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. + +## `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. + +## `defonce` +Function signature: -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. +``` +(defonce attr-map? name expr) +``` + +Works the same as [`def`](#def), but ensures that later `defonce` +calls will not override existing bindings: -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`. +``` fennel +(defonce a 10) +(defonce a 20) +(print a) ;; => prints 10 +``` + +## `empty` +Function signature: + +``` +(empty x) +``` -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`. +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: -### `when-meta` -This macros is a 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`. +``` 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. +## `fn*` +Function signature: -### `with-meta` -Attach metadata to a value. +``` +(fn* name docstring? [arglist*] body* name docstring ([arglist*] body)*) +``` - >> (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 +Create (anonymous) function of fixed arity. +Supports multiple arities by defining bodies as lists: -When metadata feature is not enabled, returns the value without additional metadata. +### Examples +Named function of fixed arity 2: +``` fennel +(fn* f [a b] (+ a b)) +``` -### `meta` -Get metadata table from object: +Function of fixed arities 1 and 2: - >> (meta (with-meta {} {:meta "data"})) - { - :meta "data" - } +``` 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: -## `def` and `defonce` -`def` is wrappers around `local` which can declare variables inside namespace, and as local at the same time: +``` fennel +(fn* f + ([] nil) + ([x & xs] + (print x) + (f (unpack xs)))) +``` - >> (def ns {}) - >> (def a 10) - >> a - 10 - >> (def ns.a 20) - >> a - 20 - >> ns.a - 20 +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. -Both `ns.a` and `a` refer to the same value. +Named functions accept additional documentation string before the +argument list: -`defonce` ensures that the binding isn't overridden by another `defonce`: +``` fennel +(fn* cube + "raise `x` to power of 3" + [x] + (^ x 3)) - >> (defonce ns {}) - >> (defonce ns.a 42) - >> (defonce ns 10) - >> ns - {:a 42} - >> a - 42 +(fn* greet + "greet a `person`, optionally specifying default `greeting`." + ([person] (print (.. "Hello, " person "!"))) + ([greeting person] (print (.. greeting ", " person "!")))) +``` -Both `def` and `defonce` support literal metadata table as first argument, or a :dynamic keyword, that uses Fennel `var` instead of `local`: +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. - >> (def {:dynamic true} a 10) - >> (set a 20) - >> a - 20 - >> (defonce :dynamic b 40) - >> (set b 42) - >> b - 42 +##### 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. -Documentation string can be attached to value via `:doc` keyword. -However it is not recommended to attach metadata to everything except tables and functions: +This roughly means, that instead of writing this: - ;; Bad, may overlap with existing documentation for 299792458, if any - >> (def {:doc "The speed of light in m/s"} c 299792458) - >> (doc c) - c - The speed of light in m/s +``` fennel +(local ns {}) - ;; OK - >> (def {:doc "default connection options"} - defaults {:port 1234 - :host localhost}) +(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*` -Clojure's `fn` equivalent. -Returns a function of fixed amount of arguments by doing runtime dispatch based on argument count. -Capable of producing multi-arity functions: +(fn ns.g [x] (f (* x 100))) ;; `g` can be defined as `ns.g` as it is only exported - (fn* square "square number" [x] (^ x 2)) +ns +``` - (square 9) ;; => 81.0 - (square 1 2) ;; => error +It is possible to write: - (fn* range - "Returns increasing sequence of numbers from `lower' to `upper'. - If `lower' is not provided, sequence starts from zero. - Accepts optional `step'" - ([upper] (range 0 upper 1)) - ([lower upper] (range lower upper 1)) - ([lower upper step] - (let [res []] - (for [i lower (- upper step) step] - (table.insert res i)) - res))) +``` fennel +(local ns {}) - (range 10) ;; => [0 1 2 3 4 5 6 7 8 9] - (range -10 0) ;; => [-10 -9 -8 -7 -6 -5 -4 -3 -2 -1] - (range 0 1 0.2) ;; => [0.0 0.2 0.4 0.6 0.8] +(fn* ns.f [x] + (if (> x 0) (f (- x 1)))) -Both variants support up to one arity with `& more`: +(fn* ns.g [x] (f (* x 100))) ;; we can use `f` here no problem - (fn* vec [& xs] xs) +ns +``` - (vec 1 2 3) ;; => [1 2 3] +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. - (fn* add - "sum two or more values" - ([] 0) - ([a] a) - ([a b] (+ a b)) - ([a b & more] (add (+ a b) (unpack more)))) +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: - (add) ;; => 0 - (add 1) ;; => 1 - (add 1 2) ;; => 3 - (add 1 2 3 4) ;; => 10 +``` fennel +(local ns {:strings {} + :tables {}}) -One extra capability of `fn*` supports the same semantic as `def` regarding namespaces: +(fn* ns.strings.join + ([s1 s2] (.. s1 s2)) + ([s1 s2 & strings] + (join (join s1 s2) (unpack strings)))) ;; call `join` resolves to ns.strings.join - (local ns {}) +(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 +``` - (fn* ns.plus - ([] 0) - ([x] x) - ([x y] (+ x y)) - ([x y & zs] (apply plus (+ x y) zs))) +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: - ns +``` fennel +(ns.strings.join "a" "b" "c") +;; => abc +(join ["a"] ["b"] ["c"] ["d" "e"]) +;; => ["a" "b" "c" "d" "e"] +(join "a" "b" "c") +;; {} +``` -Note, that `plus` is used without `ns` part, e.g. not `ns.plus`. -If we `require` this code from file in the REPL, we will see that our `ns` has single function `plus`: +## `if-let` +Function signature: - >> (local ns (require :module)) - >> ns - {add #<function 0xbada55code>} +``` +(if-let [binding test] then-branch else-branch) +``` -This is possible because `fn*` separates the namespace part from the function name, and creates a `local` variable with the same name as function, then defines the function within lexical scope of `do`, sets `namespace.foo` to it and returns the function object to the outer scope. +If test is logical true, +evaluates `then-branch` with binding-form bound to the value of test, +if not, yields `else-branch`. - (local plus - (do (fn plus [...] - ;; plus body - ) - (set ns.plus plus) - plus)) +## `if-some` +Function signature: -See `core.fnl` for more examples. +``` +(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`. -## `fn+` -Works similarly to Fennel's `fn`, by creating ordinary function without arity semantics, except does the namespace automation like `fn*`, and has the same order of arguments as the latter: +## `into` +Function signature: - (local ns {}) +``` +(into to from) +``` - ;; module & file-local functions - (fn+ ns.double - "double the number" - [x] - (* x 2)) +Transform one table into another. Mutates first table. - (fn+ ns.triple - [x] - (* x 3)) +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: - ;; no namespace, file-local function - (fn+ quadruple - [x] - (* x 4)) +``` 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} +``` - ;; anonymous file-local function - (fn+ [x] (* x 5)) +Conversion between different table types is also supported: - ns +``` fennel +(into [] {:a 1 :b 2 :c 3}) ;; => [[:a 1] [:b 2] [:c 3]] +(into {} [[:a 1] [:b 2]]) ;; => {:a 1 :b 2} +``` -See `core.fnl` for more examples. +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-let` and `when-let` -When test expression is not `nil` or `false`, evaluates the first body form with the `name` bound to the result of the expressions. +If table is empty, `into` defaults to sequential table, because it +allows safe conversion from both sequential and associative tables. - (if-let [val (test)] - (print val) - :fail) +Type for non empty tables hidden in variables can be deduced at +runtime, and this works as expected: -Expanded form: +``` 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} +``` - (let [tmp (test)] - (if tmp - (let [val tmp] - (print val)) - :fail)) +`cljlib.fnl` module provides two additional functions `vector` and +`hash-map`, that can create empty tables, which can be distinguished +at runtime: -`when-let` is mostly the same, except doesn't have false branch and accepts any amount of forms: +``` fennel +(into (vector) {:a 1 :b 2}) ;; => [[:a 1] [:b 2]] +(into (hash-map) [[:a 1 :b 2]]) ;; => {:a 1 :b 2} +``` - (when-let [val (test)] - (print val) - val) +## `meta` +Function signature: -Expanded form: +``` +(meta value) +``` - (let [tmp (test)] - (if tmp - (let [val tmp] - (print val) - val))) +Get `value` metadata. If value has no metadata, or metadata +feature is not enabled returns `nil`. +### Example -## `if-some` and `when-some` -Much like `if-let` and `when-let`, except tests expression for not being `nil`. +``` fennel +>> (meta (with-meta {} {:meta "data"})) +;; => {:meta "data"} +``` - (when-some [val (foo)] - (print (.. "val is not nil: " val)) - val) +### 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. -## `into` -Clojure's `into` function is implemented as macro, because Fennel has no runtime distinction between `[]` and `{}` tables, since Lua also doesn't feature this feature. -However we can do this at compile time. +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`. + +## `when-let` +Function signature: + +``` +(when-let [binding test] & body) +``` + +If test is logical true, +evaluates `body` in implicit `do`. + +## `when-meta` +Function signature: - (into [1 2 3] [4 5 6]) ;; => [1 2 3 4 5 6] - (into [] {:a 1 :b 2 :c 3 :d 4}) ;; => [["d" 4] ["a" 1] ["b" 2] ["c" 3]] - (into {} [[:d 4] [:a 1] [:b 2] [:c 3]]) ;; => {:a 1 :b 2 :c 3 :d 4} - (into {:a 0 :e 5} {:a 1 :b 2 :c 3 :d 4}) ;; => {:a 1 :b 2 :c 3 :d 4 :e 5} +``` +(when-meta [& body]) +``` -Because the type check at compile time it will only respect the type when literal representation is used. -If a variable holding the table, its type is checked at runtime. -Empty tables default to sequential ones: +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. - (local a []) - (into a {:a 1 :b 2}) ;; => [["b" 2] ["a" 1]] +## `when-some` +Function signature: - (local b {}) - (into b {:a 1 :b 2}) ;; => [["b" 2] ["a" 1]] +``` +(when-some [binding test] & body) +``` -However, if target table is not empty, its type can be deduced: +If test is non-`nil`, +evaluates `body` in implicit `do`. - (local a {:c 3}) - (into a {:a 1 :b 2}) ;; => {:a 1 :b 2 :c 3} +## `with-meta` +Function signature: - (local b [1]) - (into b {:a 1 :b 2}) ;; => [1 ["b" 2] ["a" 1]] +``` +(with-meta value meta) +``` -Note that when converting associative table into sequential table order is determined by the `pairs` function. -Also note that if variable stores the table has both integer key 1, and other associative keys, the type will be the same as of sequential table. +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 +``` -## `defmulti` and `defmethod` -A bit more simple implementations of Clojure's `defmulti` and `defmethod`. -`defmulti` macros returns an empty table with `__call` metamethod, that calls dispatching function on its arguments. -Methods are defined inside `multimethods` table, which is also stored in the metatable. -`defmethod` adds a new method to the metatable of given `multifn`. -It accepts the multi-fn table as its first argument, the dispatch value as second, and Fennel's arglist followed by the body: +--- - (defmulti fac (fn [x] x)) +Copyright (C) 2020 Andrey Orst - (defmethod fac 0 [_] 1) - (defmethod fac :default [x] (* x (fac (- x 1)))) +License: [MIT](https://gitlab.com/andreyorst/fennel-cljlib/-/raw/master/LICENSE) - (fac 4) ;; => 24 -`:default` is a special method which gets called when no other methods were found for given dispatch value. +<!-- Generated with Fenneldoc 0.0.4 + https://gitlab.com/andreyorst/fenneldoc --> diff --git a/doc/cljlib.md b/doc/cljlib.md index 34e764e..1bf82ac 100644 --- a/doc/cljlib.md +++ b/doc/cljlib.md @@ -17,7 +17,7 @@ This example is mapping an anonymous `function` over a table, producing new table and concatenating it with `" "`. However this library also provides Fennel-specific set of -[macros](./cljlib-macros.md), that provides additional facilites like +[macros](./cljlib-macros.md), that provides additional facilities like `fn*` or `defmulti` which extend the language allowing writing code that looks and works mostly like Clojure. @@ -31,7 +31,7 @@ brackets). Functions, which signatures look like `(foo ([x]) ([x y]) ([x y & zs]))`, it is a multi-arity function, which accepts either one, two, or three-or-more arguments. Each `([...])` represents different body -of a function which is choosed by checking amount of arguments passed +of a function which is chosen by checking amount of arguments passed to the function. See [Clojure's doc section on multi-arity functions](https://clojure.org/guides/learn/functions#_multi_arity_functions). @@ -128,7 +128,7 @@ Function signature: ``` Apply `f` to the argument list formed by prepending intervening -arguments to `args`, adn `f` must support variadic amount of +arguments to `args`, and `f` must support variadic amount of arguments. ### Examples @@ -756,8 +756,6 @@ Reduce sequence of numbers with [`add`](#add) ;; => 20 ``` - - ## `reduce-kv` Function signature: diff --git a/tests/core.fnl b/tests/core.fnl index 5dc4953..edd72f4 100644 --- a/tests/core.fnl +++ b/tests/core.fnl @@ -5,7 +5,8 @@ "Require module and bind all it's functions to locals." `(local ,(let [destr-map# {}] (each [k# _# (pairs (require module))] - (tset destr-map# k# (sym k#))) + (when (not= (string.sub k# 1 1) :_) + (tset destr-map# k# (sym k#)))) destr-map#) (require ,module))) diff --git a/tests/fn.fnl b/tests/fn.fnl index aa86b12..4381a60 100644 --- a/tests/fn.fnl +++ b/tests/fn.fnl @@ -32,13 +32,3 @@ :fnl/arglist ["\n ([x])" "\n ([x y])" "\n ([x y & z])"]})))) - -(deftest fn+ - (testing "fn+ meta" - (fn+ f "docstring" [x] x) - (assert-eq (meta f) (when-meta {:fnl/docstring "docstring" - :fnl/arglist ["x"]})) - - (fn+ f "docstring" [...] [...]) - (assert-eq (meta f) (when-meta {:fnl/docstring "docstring" - :fnl/arglist ["..."]})))) diff --git a/tests/macros.fnl b/tests/macros.fnl index a9b41fe..29b5317 100644 --- a/tests/macros.fnl +++ b/tests/macros.fnl @@ -145,7 +145,7 @@ (deftest def-macros (testing "def" - (def {:dynamic true} a 10) + (def {:mutable true} a 10) (assert-eq a 10) (set a 20) (assert-eq a 20) @@ -154,12 +154,12 @@ (def a.b 10) (assert-eq a.b 10) (assert-eq b 10) - (def :dynamic c 10) + (def :mutable c 10) (set c 15) (assert-eq c 15)) (testing "defonce" - (defonce {:dynamic true} a 10) + (defonce {:mutable true} a 10) (assert-eq a 10) (defonce a {}) (assert-eq a 10) @@ -175,7 +175,7 @@ (testing "def meta" (def {:doc "x"} x 10) (assert-eq (meta x) (when-meta {:fnl/docstring "x"})) - (def {:doc "x" :dynamic true} x 10) + (def {:doc "x" :mutable true} x 10) (assert-eq (meta x) (when-meta {:fnl/docstring "x"}))) (testing "defonce meta table" @@ -183,7 +183,7 @@ (assert-eq (meta x) (when-meta {:fnl/docstring "x"})) (defonce {:doc "y"} x 20) (assert-eq (meta x) (when-meta {:fnl/docstring "x"})) - (defonce {:doc "y" :dynamic true} y 20) + (defonce {:doc "y" :mutable true} y 20) (assert-eq (meta y) (when-meta {:fnl/docstring "y"})))) (deftest empty |