summaryrefslogtreecommitdiff
path: root/README.org
blob: 56356c1fb661076bc0870d6d2536a738b4eb8ed3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
#+title: Fennel Cljlib
[[https://gitlab.com/andreyorst/fennel-cljlib/-/commits/master][https://gitlab.com/andreyorst/fennel-cljlib/badges/master/pipeline.svg]] [[https://gitlab.com/andreyorst/fennel-cljlib/-/commits/master][https://gitlab.com/andreyorst/fennel-cljlib/badges/master/coverage.svg]]

Experimental library for [[https://fennel-lang.org/][Fennel]] language, that adds many functions from [[https://clojure.org/][Clojure]]'s standard library.
This is not a one to one port of Clojure =core=, because many Clojure functions require certain guarantees, like immutability of the underlying data structures, or laziness.
Therefore some names were changed, but they should be still recognizable, and certain functions were altered to better suit the domain.

Even though it is project is experimental, the goals of this project are:

- Have a self contained library, with no dependencies, that provides a set of useful functions from Clojure =core=,
- Be close to the platform, e.g. implement functions in a way that is efficient to use in Lua VM,
- Be well documented library, with good test coverage.


* Macros
List of macros provided by the library.

** 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.

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[fn: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=
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=.

*** =with-meta=
Attach metadata to a value.

#+begin_src 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
#+end_src

When metadata feature is not enabled, returns the value without additional metadata.

*** =meta=
Get metadata table from object:

#+begin_src fennel
  >> (meta (with-meta {} {:meta "data"}))
  {
      :meta "data"
  }
#+end_src

** =def= and =defonce=
=def= is wrappers around =local= which can declare variables inside namespace, and as local at the same time:

#+begin_src fennel
  >> (def ns {})
  >> (def a 10)
  >> a
  10
  >> (def ns.a 20)
  >> a
  20
  >> ns.a
  20
#+end_src

Both =ns.a= and =a= refer to the same value.

=defonce= ensures that the binding isn't overridden by another =defonce=:

#+begin_src fennel
  >> (defonce ns {})
  >> (defonce ns.a 42)
  >> (defonce ns 10)
  >> ns
  {:a 42}
  >> a
  42
#+end_src

Both =def= and =defonce= support literal metadata table as first argument, or a :dynamic keyword, that uses Fennel =var= instead of =local=:

#+begin_src fennel
  >> (def {:dynamic true} a 10)
  >> (set a 20)
  >> a
  20
  >> (defonce :dynamic b 40)
  >> (set b 42)
  >> b
  42
#+end_src

Documentation string can be attached to value via =:doc= keyword.
However it is not recommended to attach metadata to everything except tables and functions:

#+begin_src fennel
  ;; 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

  ;; OK
  >> (def {:doc "default connection options"}
          defaults {:port 1234
                    :host localhost})
#+end_src

** =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:

#+begin_src fennel
  (fn* square "square number" [x] (^ x 2))

  (square 9) ;; => 81.0
  (square 1 2) ;; => error

  (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)))

  (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]
#+end_src

Both variants support up to one arity with =& more=:

#+begin_src fennel
  (fn* vec [& xs] xs)

  (vec 1 2 3) ;; => [1 2 3]

  (fn* add
    "sum two or more values"
    ([] 0)
    ([a] a)
    ([a b] (+ a b))
    ([a b & more] (add (+ a b) (unpack more))))

  (add) ;; => 0
  (add 1) ;; => 1
  (add 1 2) ;; => 3
  (add 1 2 3 4) ;; => 10
#+end_src

One extra capability of =fn*= supports the same semantic as =def= regarding namespaces:

#+begin_src fennel
  (local ns {})

  (fn* ns.plus
    ([] 0)
    ([x] x)
    ([x y] (+ x y))
    ([x y & zs] (apply plus (+ x y) zs)))

  ns
#+end_src

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=:

#+begin_src fennel
  >> (local ns (require :module))
  >> ns
  {add #<function 0xbada55code>}
#+end_src

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.

#+begin_src fennel
  (local plus
         (do (fn plus [...]
               ;; plus body
               )
             (set ns.plus plus)
             plus))
#+end_src

See =core.fnl= for more examples.

** =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:

#+begin_src fennel
  (local ns {})

  ;; module & file-local functions
  (fn& ns.double
    "double the number"
    [x]
    (* x 2))

  (fn& ns.triple
    [x]
    (* x 3))

  ;; no namespace, file-local function
  (fn& quadruple
    [x]
    (* x 4))

  ;; anonymous file-local function
  (fn& [x] (* x 5))

  ns
#+end_src

See =core.fnl= for more examples.

** =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.

#+begin_src fennel
  (if-let [val (test)]
    (print val)
    :fail)
#+end_src

Expanded form:

#+begin_src fennel
  (let [tmp (test)]
    (if tmp
        (let [val tmp]
          (print val))
        :fail))
#+end_src

=when-let= is mostly the same, except doesn't have false branch and accepts any amount of forms:

#+begin_src fennel
  (when-let [val (test)]
    (print val)
    val)
#+end_src

Expanded form:

#+begin_src fennel
  (let [tmp (test)]
    (if tmp
        (let [val tmp]
          (print val)
          val)))
#+end_src

** =if-some= and =when-some=
Much like =if-let= and =when-let=, except tests expression for not being =nil=.

#+begin_src fennel
  (when-some [val (foo)]
    (print (.. "val is not nil: " val))
    val)
#+end_src

** =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.

#+begin_src fennel
  (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}
#+end_src

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:

#+begin_src fennel
  (local a [])
  (into a {:a 1 :b 2}) ;; => [["b" 2] ["a" 1]]

  (local b {})
  (into b {:a 1 :b 2}) ;; => [["b" 2] ["a" 1]]
#+end_src

However, if target table is not empty, its type can be deduced:

#+begin_src fennel
  (local a {:c 3})
  (into a {:a 1 :b 2}) ;; => {:a 1 :b 2 :c 3}

  (local b [1])
  (into b {:a 1 :b 2}) ;; => [1 ["b" 2] ["a" 1]]
#+end_src

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.

** =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:

#+begin_src fennel
  (defmulti fac (fn [x] x))

  (defmethod fac 0 [_] 1)
  (defmethod fac :default [x] (* x (fac (- x 1))))

  (fac 4) ;; => 24
#+end_src

=:default= is a special method which gets called when no other methods were found for given dispatch value.


* Functions
Here are some important functions from the library.
Full set can be examined by requiring the module.

** =seq=
=seq= produces a sequential table from any kind of table in linear time.
Works mostly like in Clojure, but, since Fennel doesn't have list object, it returns sequential table or =nil=:

#+begin_src fennel
  (seq [1 2 3 4 5]) ;; => [1 2 3 4 5]
  (seq {:a 1 :b 2 :c 3 :d 4})
  ;; => [["d" 4] ["a" 1] ["b" 2] ["c" 3]]
  (seq []) ;; => nil
  (seq {}) ;; => nil
#+end_src

See =into= on how to transform such sequence back into associative table.

** =first=, =last=, =butlast=, and =rest=
=first= returns first value of a table.
It call =seq= on it, so this takes linear time for any kind of table.
As a consequence, associative tables are supported:

#+begin_src fennel
  (first [1 2 3]) ;; => 1
  (first {:host "localhost" :port 2344 :options {}})
  ;; => ["host" "localhost"]
#+end_src

=last= returns the last argument from table:

#+begin_src fennel
  (last [1 2 3]) ;; => 3
  (last {:a 1 :b 2}) ;; => [:b 2]
#+end_src

=butlast= returns everything from the table, except the last item:

#+begin_src fennel
  (butlast [1 2 3]) ;; => [1 2]
#+end_src

=rest= works the same way, but returns everything except first item of a table.

#+begin_src fennel
  (rest [1 2 3]) ;; => [2 3]
  (rest {:host "localhost" :port 2344 :options {}})
  ;; => [["port" 2344] ["options" {}]]
#+end_src

All these functions call =seq= on its argument, therefore expect everything to happen in linear time.
Because of that these functions are expensive, therefore should be avoided when table type is known beforehand, and the table can be manipulated with =.= or =get=.

** =conj= and =cons=
Append and prepend item to the table.
Unlike Clojure, =conj=, and =cons= modify table passed to these functions.
This is done both to avoid copying of whole thing, and because Fennel doesn't have immutability guarantees.

=cons= accepts value as its first argument and table as second, and puts value to the front of the table:

#+begin_src fennel
  (cons 1 [2 3]) ;; => [1 2 3]
#+end_src

=conj= accepts table as its first argument and any amount of values afterwards.
It puts values in order given into the table:

#+begin_src fennel
  (conj [] 1 2 3) ;; => [1 2 3]
#+end_src

It is also possible to add items to associative table:

#+begin_src fennel
  (conj {:a 1} [:b 2]) ;; => {:a 1 :b 2}
  (conj {:a 1} [:b 2] [:a 0]) ;; => {:a 0 :b 2}
#+end_src

Both functions return the resulting table, so it is possible to nest calls to both of these.
As an example, here's a classic map function:

#+begin_src fennel
  (fn map [f col]
    (if-some [val (first col)]
      (cons (f val) (map f (rest col)))
      []))
#+end_src

=col= is not modified by the =map= function described above, but the =[]= table in the =else= branch of =is-some= is eventually modified by the stack of calls to =cons=.
However this library provides more efficient versions of map, that support arbitrary amount of tables.

** =mapv=
Mapping function over table.
In Clojure we have a =seq= abstraction, that allows us to use single =mapv= on both vectors, and hash tables.
In this library the =seq= function is implemented in a similar way, so you can expect =mapv= to behave similarly to Clojure:

#+begin_src fennel
  (fn cube [x] (* x x x))
  (mapv cube [1 2 3]) ;; => [1 8 27]

  (mapv #(* $1 $2) [1 2 3] [1 -1 0]) ;; => [1 -2 0]

  (mapv (fn [f-name s-name company position]
          (.. f-name " " s-name " works as " position " at " company))
        ["Bob" "Alice"]
        ["Smith" "Watson"]
        ["Happy Days co." "Coffee With You"]
        ["secretary" "chief officer"])
  ;; => ["Bob Smith works as secretary at Happy Days co."
  ;;     "Alice Watson works as chief officer at Coffee With You"]

  (mapv (fn [[k v]] [(string.upper k) v]) {:host "localhost" :port 1344})
  ;; => [["HOST" "localhost"] ["PORT" 1344]]
#+end_src

** =reduce= and =reduce-kv=
Ordinary reducing functions.
Work the same as in Clojure, except doesn't yield transducer when only function was passed.

#+begin_src fennel
  (fn add [a b] (+ a b))
  (reduce add [1 2 3 4 5]) ;; => 15
  (reduce add 10 [1 2 3 4 5]) ;; => 25
#+end_src

=reduce-kv= expects function that accepts 3 arguments and initial value.
Then it maps function over the associative map, by passing initial value as a first argument, key as second argument, and value as third argument.

#+begin_src fennel
  (reduce-kv (fn [acc key val]
               (if (or (= key :a) (= key :c))
                 (+ acc val) acc))
             0
             {:a 10 :b -20 :c 10})
  ;; => 20
#+end_src

** Predicate functions
Set of functions, that are small but useful with =mapv= or =reduce=.
These are commonly used so it makes sense to have that, without defining via anonymous function or =#= shorthand every time.

- =map?= - check if table is an associative table.
  Returns =false= for empty table.
- =seq?= - check if table is a sequential table
  Returns =false= for empty table.

Other predicates are self-explanatory:

- =assoc?=
- =boolean?=
- =double?=
- =empty?=
- =even?=
- =false?=
- =int?=
- =neg?=
- =nil?=
- =odd?=
- =pos?=
- =string?=
- =true?=
- =zero?=

** =eq=
Deep compare values.
If given two tables, recursively calls =eq= on each field until one of the tables exhausted.
Other values are compared with default equality operator.

** =comp=
Compose functions into one function.

#+begin_src fennel
  (fn square [x] (^ x 2))
  (fn inc [x] (+ x 1))

  ((comp square inc) 5) ;; => 36
#+end_src

#  LocalWords:  Luajit VM arity runtime multi Cljlib fn mapv kv REPL
#  LocalWords:  namespaced namespace eq metatable Lua defonce arglist
#  LocalWords:  namespaces defmulti defmethod metamethod butlast
#  LocalWords:  prepend LocalWords docstring

** =every?= and =not-any?=
=every?= checks if predicate is true for every item in the table.
=not-any?= checks if predicate is false foe every item in the table.

#+begin_src fennel
  >> (every? pos-int? [1 2 3 4])
  true
  >> (not-any? pos-int? [-1 -2 -3 4.2])
  true
#+end_src

* Footnotes
[fn:1] https://todo.sr.ht/~technomancy/fennel/18#event-56799