summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrey Orst <andreyorst@gmail.com>2020-10-21 20:34:39 +0300
committerAndrey Orst <andreyorst@gmail.com>2020-10-21 20:34:39 +0300
commit58c188560c2935d500852ebb03f00f832c61cc72 (patch)
tree6e783535473649b7d44c7eb80b603e0a06dca826
parent46f472901768d53ad62f9313a977c5ff006a041c (diff)
added more macros, and functions to the `core` modules
-rw-r--r--README.org2
-rw-r--r--core.fnl119
-rw-r--r--core_test.fnl43
-rw-r--r--macros/core.fnl56
-rw-r--r--macros/fn.fnl156
5 files changed, 306 insertions, 70 deletions
diff --git a/README.org b/README.org
index e681f26..3b5d769 100644
--- a/README.org
+++ b/README.org
@@ -12,7 +12,7 @@ Goals of this project are:
** Macros
*** =fn*=
-Clojure's =defn= equivalent.
+Clojure's =fn= equivalent.
Returns a function of fixed arity by doing runtime dispatch, based on argument amount.
Capable of producing multi-arity functions:
diff --git a/core.fnl b/core.fnl
index b4875e6..1c84b26 100644
--- a/core.fnl
+++ b/core.fnl
@@ -2,13 +2,27 @@
(local _unpack (or table.unpack unpack))
(import-macros {: fn*} :macros.fn)
+(fn seq [tbl]
+ "Return sequential table.
+Transforms original table to sequential table of key value pairs
+stored as sequential tables in linear time. If original table is
+sequential table, leaves it unchanged."
+ (var assoc? false)
+ (let [res []]
+ (each [k v (pairs tbl)]
+ (if (and (not assoc?)
+ (not (= (type k) "number")))
+ (set assoc? true))
+ (insert res [k v]))
+ (if assoc? res tbl)))
+
(fn first [itbl]
"Return first element of an indexed table."
(. itbl 1))
(fn rest [itbl]
- "Returns table of all elements of inexed table but the first one."
+ "Returns table of all elements of indexed table but the first one."
(let [[_ & xs] itbl]
xs))
@@ -17,7 +31,7 @@
"Insert `x' as a last element of indexed table `itbl'. Modifies `itbl'"
([] [])
([itbl] itbl)
- ([itbl x] (insert itbl x) itbl)
+ ([itbl x] (doto itbl (insert x)))
([itbl x & xs]
(if (> (length xs) 0)
(let [[y & xs] xs] (conj (conj itbl x) y (_unpack xs)))
@@ -28,23 +42,26 @@
"Like conj but joins at the front. Modifies `itbl'."
([] [])
([itbl] itbl)
- ([itbl x] (insert itbl 1 x) itbl)
+ ([itbl x] (doto itbl (insert 1 x)))
([itbl x & xs]
(if (> (length xs) 0)
(let [[y & xs] xs] (consj (consj itbl x) y (_unpack xs)))
(consj itbl x))))
-(fn cons [x itbl]
+(fn* cons [x itbl]
"Insert `x' to `itbl' at the front. Modifies `itbl'."
(doto (or itbl [])
(insert 1 x)))
(fn* reduce
- "Reduce collection using function of two arguments and optional initial value.
+ "Reduce indexed table using function `f' and optional initial value `val'.
+
+([f table])
+([f val table])
-f should be a function of 2 arguments. If val is not supplied,
+`f' should be a function of 2 arguments. If val is not supplied,
returns the result of applying f to the first 2 items in coll, then
applying f to that result and the 3rd item, etc. If coll contains no
items, f must accept no arguments as well, and reduce returns the
@@ -65,12 +82,27 @@ val and f is not called."
(reduce f (f val x) xs)
val)))
+(fn* reduce-kv
+ "Reduces an associative table using function `f' and initial value `val'.
+
+([f val table])
+
+`f' should be a function of 3 arguments. Returns the result of
+applying `f' to `val', the first key and the first value in coll, then
+applying `f' to that result and the 2nd key and value, etc. If coll
+contains no entries, returns `val' and `f' is not called. Note that
+reduce-kv is supported on vectors, where the keys will be the
+ordinals." [f val kvtbl]
+ (var res val)
+ (each [k v (pairs kvtbl)]
+ (set res (f res k v)))
+ res)
(fn* mapv
"Maps function `f' over indexed tables.
Accepts arbitrary amount of tables. Function `f' must take the same
-amount of parameters as the amount of tables passed to `mapv'. Applyes
+amount of parameters as the amount of tables passed to `mapv'. Applies
`f' over first value of each table. Then applies `f' to second value
of each table. Continues until any of the tables is exhausted. All
remaining values are ignored. Returns a table of results. "
@@ -110,12 +142,14 @@ remaining values are ignored. Returns a table of results. "
(insert res (f (_unpack v))))
res)))
+
(fn kvseq [kvtbl]
(let [res []]
(each [k v (pairs kvtbl)]
(insert res [k v]))
res))
+
(fn* mapkv
"Maps function `f' over one or more associative tables.
@@ -124,38 +158,67 @@ supplied, `f' must take double the table amount of arguments. Returns
indexed table of results. Order of results depends on the order
returned by the `pairs' function. If you want consistent results, consider
sorting tables first."
-
([f kvtbl]
- (local res [])
- (each [k v (pairs kvtbl)]
- (insert res (f k v)))
- res)
- ([f & kvtbls]
- (local itbls [])
+ (let [res []]
+ (each [k v (pairs kvtbl)]
+ (insert res (f k v)))
+ res))
+ ([f kvtbl & kvtbls]
+ (local itbls [(kvseq kvtbl)])
(each [_ t (ipairs kvtbls)]
(insert itbls (kvseq t)))
(mapv f (_unpack itbls))))
-(fn eq2 [a b]
- (if (and (= (type a) "table") (= (type b) "table"))
- (and (reduce #(and $1 $2) (mapkv (fn [k v] (eq2 (. b k) v)) a))
- (reduce #(and $1 $2) (mapkv (fn [k v] (eq2 (. a k) v)) b)))
- (= a b)))
-
(fn* eq?
"Deep compare values."
- [x & xs]
- (reduce #(and $1 $2) (mapv #(eq2 x $) xs)))
-
-
-{: mapv
+ ([x] true)
+ ([x y]
+ (if (and (= (type x) "table") (= (type y) "table"))
+ (and (reduce #(and $1 $2) (mapv (fn [[k v]] (eq? (. y k) v)) (kvseq x)))
+ (reduce #(and $1 $2) (mapv (fn [[k v]] (eq? (. x k) v)) (kvseq y))))
+ (= x y)))
+ ([x y & xs]
+ (reduce #(and $1 $2) (eq? x y) (mapv #(eq? x $) xs))))
+
+;;;;;;;;;; fn stuff ;;;;;;;;
+(fn identity [x] x)
+
+(fn* comp
+ ([] identity)
+ ([f] f)
+ ([f g]
+ (fn*
+ ([] (f (g)))
+ ([x] (f (g x)))
+ ([x y] (f (g x y)))
+ ([x y z] (f (g x y z)))
+ ([x y z & args] (f g x y z (_unpack args)))))
+ ([f g & fs]
+ (reduce comp (conj [f g] (_unpack fs)))))
+
+(fn* every?
+ [pred itbl]
+ (if (= 0 (length itbl)) true
+ (pred (first itbl)) (every? pred (rest itbl))
+ false))
+
+(fn* some
+ [pred itbl]
+ (if (> (length itbl) 0)
+ ))
+
+{: seq
+ : mapv
: mapkv
: reduce
+ : reduce-kv
: conj
: cons
+ : consj
: first
: rest
- : eq?}
-
-;; (local {: mapv : mapkv : reduce : conj : cons : first : rest : eq?} (require :core))
+ : eq?
+ : identity
+ : comp
+ : every?}
diff --git a/core_test.fnl b/core_test.fnl
new file mode 100644
index 0000000..eb58a44
--- /dev/null
+++ b/core_test.fnl
@@ -0,0 +1,43 @@
+(import-macros {: fn*} :macros.fn)
+
+(local {: seq
+ : mapv
+ : mapkv
+ : reduce
+ : reduce-kv
+ : conj
+ : cons
+ : consj
+ : first
+ : rest
+ : eq?
+ : identity
+ : comp
+ : every?} (require :core))
+
+;; Test equality function should be done first and with a lot of care,
+;; because we rely on deep comparison in other tests.
+
+(assert (eq? 1 1))
+(assert (not (eq? 1 2)))
+(assert (eq? 1 1 1 1 1))
+(assert (eq? "1" "1" "1" "1" "1"))
+(assert (eq? [1 2] [1 2]))
+(assert (not (eq? [1] [1 2])))
+(assert (not (eq? [1 2] [1])))
+(assert (eq? [1 [2]] [1 [2]] [1 [2]]))
+(assert (eq? [1 [2]] [1 [2]] [1 [2]]))
+(assert (not (eq? [1 [2]] [1 [2]] [1 [2 [3]]])))
+
+(fn* range
+ ([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)))
+
+(assert (eq? (range 10) [0 1 2 3 4 5 6 7 8 9]))
+(assert (eq? (range -5 5) [-5 -4 -3 -2 -1 0 1 2 3 4]))
+;; (assert (eq? (range 0 1 0.2) [0 0.2 0.4 0.6 0.8])) ;; TODO: fails, unsure why.
diff --git a/macros/core.fnl b/macros/core.fnl
new file mode 100644
index 0000000..e88d575
--- /dev/null
+++ b/macros/core.fnl
@@ -0,0 +1,56 @@
+(import-macros {: fn*} :macros.fn)
+(local _unpack (or table.unpack unpack))
+
+(fn check-bindings [bindings]
+ (assert-compile (sequence? bindings) "expected binding table
+
+* Try placing a table here in square brackets containing identifiers to bind." bindings)
+ (assert-compile (= (length bindings) 2) "expected exactly two forms in binding vector." bindings))
+
+(fn* if-let
+ ([bindings then]
+ (if-let bindings then 'nil))
+ ([bindings then else]
+ (check-bindings bindings)
+ (let [[form test] bindings]
+ `(let [tmp# ,test]
+ (if tmp#
+ (let [,form tmp#]
+ ,then)
+ ,else)))))
+
+(fn* when-let
+ [bindings & body]
+ (check-bindings bindings)
+ (let [[form test] bindings]
+ `(let [tmp# ,test]
+ (if tmp#
+ (let [,form tmp#]
+ ,(_unpack body))))))
+
+(fn* if-some
+ ([bindings then]
+ (if-some bindings then 'nil))
+ ([bindings then else]
+ (check-bindings bindings)
+ (let [[form test] bindings]
+ `(let [tmp# ,test]
+ (if (= tmp# nil)
+ ,else
+ (let [,form tmp#]
+ ,then))))))
+
+(fn* when-some
+ [bindings & body]
+ (check-bindings bindings)
+ (let [[form test] bindings]
+ `(let [tmp# ,test]
+ (if (= tmp# nil)
+ nil
+ (let [,form tmp#]
+ ,(_unpack body))))))
+
+{: if-let
+ : when-let
+ : if-some
+ : when-some}
diff --git a/macros/fn.fnl b/macros/fn.fnl
index 7d12008..7a3b027 100644
--- a/macros/fn.fnl
+++ b/macros/fn.fnl
@@ -5,55 +5,73 @@
(= (type x) "string"))
(fn has-amp? [args]
- "Check if arglist has `&' and return its position of `false'.
-Performs additional checks for `&' 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) "&")
(if res (assert-compile false "only one `&' can be specified in arglist." args)
(set res i))
+ (= (tostring s) "...")
+ (assert-compile false "use of `...' in `fn*' is not permitted." args)
(and res (> i (+ res 1)))
- (assert-compile false "only one `more' arg can be supplied after `&' in arglist." args)))
+ (assert-compile false "only one `more' argument can be supplied after `&' in arglist." args)))
res)
(fn gen-arity [[args & body]]
- "Forms three values, representing data needed to create dispatcher:
-
-- the lengs of arglist;
-- the body of the function we generate;
-- position of `&' in the arglist. "
+ ;; Forms three values, representing data needed to create dispatcher:
+ ;;
+ ;; - the length of arglist;
+ ;; - the body of the function we generate;
+ ;; - 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)
(values (length args)
- (list 'let [args ['...]] (_unpack body))
+ (list 'let [args ['...]] (list 'do (_unpack body)))
(has-amp? args)))
-(fn arity-dispatcher [len fixed amp-body name]
- "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 represens the length of the current argumen
-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'.
-
-`amp-body' stores size of fixed part of arglist, that is, everything up until `&'"
+(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.
+ ;;
+ ;; `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'.
+ ;;
+ ;; `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.
(let [bodies '(if)]
- (each [i body (pairs (doto fixed))]
- (insert bodies (list '= len i))
+ (var max nil)
+ (each [fixed-len body (pairs (doto fixed))]
+ (when (or (not max) (> fixed-len max))
+ (set max fixed-len))
+ (insert bodies (list '= len fixed-len))
(insert bodies body))
- (when amp-body
- (let [[i body] amp-body]
- (insert bodies (list '>= len (- i 1)))
+ (when body&
+ (let [[more-len body arity] body&]
+ (assert-compile (not (and max (<= more-len max))) "fn*: arity with `& more' must have more arguments than maximum arity without `& more'.
+
+* Try adding more arguments before `&'" arity)
+ (insert bodies (list '>= len (- more-len 1)))
(insert bodies body)))
(insert bodies (list 'error
(.. "wrong argument amount"
- (if name (.. " for " name) "")) 3))
+ (if name (.. " for " name) "")) 2))
bodies))
(fn single-arity-body [args fname]
+ ;; Produces arglist and body for single-arity function.
+ ;; For more info check `gen-arity' documentation.
(let [[args & body] args
(arity body amp) (gen-arity [args (_unpack body)])]
`(let [len# (length [...])]
@@ -64,28 +82,82 @@ list, and is computed at runtime.
fname))))
(fn multi-arity-body [args fname]
- (let [bodies {}
- amp-bodies {}]
+ ;; Produces arglist and all body forms for multi-arity function.
+ ;; For more info check `gen-arity' documentation.
+ (let [bodies {} ;; bodies of fixed arity
+ bodies& []] ;; bodies where arglist contains `&'
(each [_ arity (ipairs args)]
(let [(n body amp) (gen-arity arity)]
(if amp
- (do (insert amp-bodies amp)
- (insert amp-bodies body)
- (insert amp-bodies arity))
+ (insert bodies& [amp body arity])
(tset bodies n body))))
- (assert-compile (<= (length amp-bodies) 3)
- "fn* must have only one arity with &:"
- (. amp-bodies (length amp-bodies)))
+ (assert-compile (<= (length bodies&) 1)
+ "fn* must have only one arity with `&':"
+ (. bodies& (length bodies&)))
`(let [len# (length [...])]
,(arity-dispatcher
'len#
bodies
- (if (~= (next amp-bodies) nil)
- amp-bodies)
+ (if (~= (next bodies&) nil)
+ (. bodies& 1))
fname))))
(fn fn* [name doc? ...]
- (assert-compile (not (string? name)) "fn* expects symbol as function name" name)
+ "Create (anonymous) function of fixed arity.
+Supports multiple arities by defining bodies as lists:
+
+Named function of fixed arity 2:
+(fn* f [a b] (+ a b))
+
+Function of fixed arities 1 and 2:
+(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:
+(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.
+
+Named functions accept additional documentation string before the
+argument list:
+
+(fn* cube
+ \"raise `x' to power of 3\"
+ [x]
+ (^ x 3))
+
+(fn* greet
+ \"greet a `person', optionally specifying default `greeting'.\"
+ ([person] (print (.. \"Hello, \" person \"!\")))
+ ([greeting person] (print (.. greeting \", \" person \"!\"))))
+
+Note that functions created with `fn*' when inspected with `doc'
+command will always show its arguments as `...', because the
+resulting function actually accepts variable amount of arguments, but
+we check the amount and doing destructuring in runtime.
+
+(doc greet)
+
+(greet ...)
+ greet a `person', optionally specifying default `greeting'.
+
+When defining multi-arity functions it is handy to include accepted
+arities in the docstring.
+
+Argument lists follow the same destruction rules as in `let'.
+Variadic arguments with `...' are not supported.
+
+Passing `nil' as an argument to such function breaks arity checks,
+because result of calling `length' on a indexed table with `nil' in it
+is unpredictable."
+ (assert-compile (not (string? name)) "fn* expects symbol, vector, or list as first argument" name)
(let [docstring (if (string? doc?) doc? nil)
fname (if (sym? name) (tostring name))
args (if (sym? name)
@@ -94,12 +166,14 @@ list, and is computed at runtime.
[x] args
body (if (sequence? x) (single-arity-body args fname)
(list? x) (multi-arity-body args fname)
- (assert-compile false "fn* expects vector as its arguments" x))]
+ (assert-compile false "fn*: expected parameters table.
+
+* Try adding function parameters as a list of identifiers in brackets." x))]
(if (sym? name)
`(fn ,name [...] ,docstring ,body)
`(fn [...] ,docstring ,body))))
{: fn*}
-;; (import-macros {: fn*} :fn)
-;; (fn* f ([a] a) ([a b] (+ a b)))
+;; LocalWords: arglist fn runtime arities arity multi destructuring
+;; LocalWords: docstring Variadic LocalWords