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.