summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrey Orst <andreyorst@gmail.com>2020-11-17 20:43:37 +0300
committerAndrey Orst <andreyorst@gmail.com>2020-11-17 20:43:37 +0300
commit8a136eaa444f4569fced95fb175ca41b1e8e9b94 (patch)
tree65a3b02ff65150cb2f2e5656b98ab832be39f421
parent5753537138f6a0c71ddb6a8d9770b7cb13c1bd3f (diff)
feature: major overhaul of documentation, and some housekeeping
- remove(macros): `fn+` as it seem impractical, and `fn*` can be used instead - rename(def, defonce): `:dynamic` to `:mutable` as dynamic implies dynamic scoping which is not feature of Lua. - doc: generated documentation for macro module.
-rw-r--r--cljlib-macros.fnl647
-rw-r--r--cljlib.fnl12
-rw-r--r--doc/cljlib-macros.md631
-rw-r--r--doc/cljlib.md8
-rw-r--r--tests/core.fnl3
-rw-r--r--tests/fn.fnl10
-rw-r--r--tests/macros.fnl10
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
diff --git a/cljlib.fnl b/cljlib.fnl
index a305e0a..598d4b5 100644
--- a/cljlib.fnl
+++ b/cljlib.fnl
@@ -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