Hygenic eval in Janet
Open these while you read: Janet special forms, Janet core API
Let’s say you have the following Janet program that parses the argument list.
(def positional @[])
(def args (dyn *args*))
(def flags @{})
(defn shift [] (array/remove args 0))
(shift) # remove script name
(while (not (empty? args))
(cond
(= (args 0) "--") (do
(array/concat positional args)
(break))
(= (args 0) "--file") (do
(set (flags :file) (try (args 1) ([_] (error "no argument after --file"))))
(shift)
(shift))
(string/has-prefix? "--file=" (args 0)) (do
(set (flags :file) (string/slice (args 0) (length "--file=")))
(shift))
(do
(array/push positional (args 0))
(shift))))
(printf "%q %q" positional flags)
Save the file as argparse.janet
and run it as janet argparse.janet --file foo bar
.
The code works fine. However, it can only handle --file
.
So you decided to make this into a function and get data out this way:
(def [positional flags] (parse (array/slice (dyn *args*) 1) [[:file "--file"]]))
How would you write such a function? cond
accept fixed number of branches, so you can’t use that any more ;)
Here’s what I came up with instead:
(defn parse [args opts]
(def positional @[])
(def flags @{})
(defn shift [] (array/remove args 0))
(while (not (empty? args))
(when (= (args 0) "--")
(array/concat positional args)
(break))
(prompt :a
(each [kw optname] opts
(when (= (args 0) optname)
(set (flags kw) (try (args 1) ([_] (error (string "no argument after " optname)))))
(shift)
(shift)
(return :a))
(when (string/has-prefix? (string optname "=") (args 0))
(set (flags kw) (string/slice (args 0) (inc (length optname))))
(shift)
(return :a)))
(array/push positional (args 0))
(shift)))
[positional flags])
(def [positional flags] (parse (array/slice (dyn *args*) 1) [[:file "--file"]]))
(printf "%q %q" positional flags)
Horrible! In additional to being less readable, it requires changing the code structure entirely.
Suddenly, you remember that Janet is a list processing language! Code as data, data as code. Rewrite the function, now with eval
.
(defn parse [args opts]
(eval ~(do
(def positional @[])
(def flags @{})
(def args ,args) # janet eval is hygenic
(defn shift [] (array/remove args 0))
(while (not (empty? args))
(cond
(= (args 0) "--") (do
(array/concat positional args)
(break))
,;(mapcat
(fn [[kw optname]]
(def optname= (string optname "="))
(def errstr (string "no argument after " optname))
~((= (args 0) ,optname) (do
(set (flags :file) (try (args 1) ([_] (error ,errstr))))
(shift)
(shift))
(string/has-prefix? ,optname= (args 0)) (do
(set (flags :file) (string/slice (args 0) ,(length optname=)))
(shift))))
opts)
(do
(array/push positional (args 0))
(shift))))
[positional flags])))
(def [positional flags] (parse (array/slice (dyn *args*) 1) [[:file "--file"]]))
(printf "%q %q" positional flags)
EEEEEEEEEExcellent!
I hope this article has inspired you to use a homoiconic language like Janet. Preferably Janet.
Note that the hygenic property of eval
is rather a side effect, due to how Janet code is compiled into bytecode before execution. Top-level def
will modify the current environment, while lexical def
will be compiled into stack variables.