Andrey Listopadov

require-fennel.el

~3 minutes read

A talk about an Emacs package that does everything you didn’t knew you wanted!

(probably)

Why

  • Have you ever wanted to call Fennel from Emacs Lisp?
  • Have you ever felt that the lack of functions in Lua is preventing you from achieving pure greatness?
  • Do you want to extend your Emacs with Fennel?
  • Or maybe to extend Fennel with Emacs Lisp?

I got you covered!

Calling Fennel from Emacs Lisp

Loading the Fennel compiler into Emacs:

(require-fennel fennel)

Result:

fennel

Evaluating arbitrary Fennel code from Emacs Lisp:

(fennel.eval "(values [{:foo (+ 1 2 3) :a 2}] 1 2 3)")

Result:

([(("a" . 2) ("foo" . 6))] 1 2 3)

Brief explanation of how this works

  • A separate process is started upon first call to require-fennel
  • All other calls to require-fennel reuse the same process
  • Package path is populated with buffer’s working directory
  • If a module exports a table with functions, such functions are bound to Emacs Lisp functions.
  • All other values are bound to constants
  • Fennel values are translated to Emacs Lisp and vice versa

Data conversion

Fennel Elisp
[1 2 3] [1 2 3]
{:foo 1 :bar 2} (("foo" . 1) ("bar" . 2))
{:foo 1 :bar 2} #s(hash-table test equal data ("foo" 1 "bar" 2))
(values 1 2 3) (1 2 3)
(fn [...] ...) (lambda (&rest args) args)
userdata "#<udata: 0x55b66bd6ac28>"
true t
nil, false nil

Data conversion

Elisp Fennel
[1 2 3] [1 2 3]
(1 2 3) [1 2 3]
((:foo . 1) (:bar . 2)) {:foo 1 :bar 2}
#s(hash-table test equal data ("foo" 1 "bar" 2)) {:foo 1 :bar 2}
:foo "foo"
'false false
"foo" "foo"
#'function (fn [...] (Elisp.call.function ...))
(lambda (&rest args) args) (fn [...] (Elisp.call.anon-fn-gensym ...))

Libraries!

Fennel Cljlib

(let ((default-directory "/home/alist/Projects/Archive/GitLab/fennel-cljlib/"))
  (require-fennel io.gitlab.andreyorst.cljlib :as cljlib))

Calling cljlib.conj from Emacs Lisp:

(cljlib.conj [1 2 3] 4)

Result:

[1 2 3 4]

JSON

(let ((default-directory "/home/alist/Projects/Fennel/json.fnl/target/"))
  (require-fennel io.gitlab.andreyorst.json :as json))

Encoding an Emacs Lisp alist as JSON:

(json.encode '((:a . 1) (:b . 2)))

Result:

"{\"b\": 2, \"a\": 1}"

Decoding JSON as an alist:

(json.decode "{\"a\": 1, \"b\": 2}")

Result:

(("a" . 1) ("b" . 2))

fnl-http

(let ((default-directory "/home/alist/Projects/Fennel/fnl-http/target/"))
  (require-fennel io.gitlab.andreyorst.fnl-http :as fnl-http))

Inspecting a value:

(pp-to-string (assoc "get" fnl-http.client))

Result:

("get"
 . #[(&rest args)
     ((with-current-buffer require-fennel--repl-buffer
        (let*
            (err_16686_
             (values
              (fennel-proto-repl-send-message-sync :eval
                                                   (format
                                                    "((. fennel-elisp-515-exports \"get\") %s)"
                                                    (mapconcat
                                                     #'require-fennel--elisp-to-fennel
                                                     args " "))
                                                   (lambda
                                                     (_ msg trace)
                                                     (setq err_16686_
                                                           (if trace
                                                               (format
                                                                "%s\n%s"
                                                                msg
                                                                trace)
                                                             msg)))
                                                   (lambda (data)
                                                     (message "%s"
                                                              data))
                                                   (or
                                                    require-fennel-timeout
                                                    most-positive-fixnum))))
          (if err_16686_ (error err_16686_)
            (thread-last
              values
              (mapcar
               (lambda (value)
                 (require-fennel--fennel-to-elisp value nil)))
              require-fennel--handle-multivalue-return)))))
     nil])

Defining and calling fnl-http.client.get as a function:

(defalias 'fnl-http.client.get
  (cdr (assoc "get" fnl-http.client)))

(let ((response (fnl-http.client.get "http://httpbin.org/get" '((:as . :json)))))
  (pp-to-string (assoc-delete-all "http-client" response)))

Result:

(("status" . 200) ("reason-phrase" . "OK")
 ("body"
  ("headers"
   ("X-Amzn-Trace-Id" . "Root=1-67706027-1342912f348d962951394565")
   ("Host" . "httpbin.org"))
  ("origin" . "185.165.163.84") ("args" . [])
  ("url" . "http://httpbin.org/get"))
 ("trace-redirects" . [])
 ("protocol-version" ("major" . 1) ("minor" . 1) ("name" . "HTTP"))
 ("headers" ("Content-Type" . "application/json")
  ("Access-Control-Allow-Origin" . "*") ("Content-Length" . "199")
  ("Connection" . "keep-alive")
  ("Access-Control-Allow-Credentials" . t)
  ("Server" . "gunicorn/19.9.0")
  ("Date" . "Sat, 28 Dec 2024 20:31:35 GMT"))
 ("request-time" . 140) ("length" . 199))

Calling Emacs Lisp from Fennel

(fn []
  (string.reverse
   (Elisp.call.emacs-version)))

Loading the file above into Emacs:

(require-fennel example)

(example)

Result:

"52-21-4202 fo
)8.71.1 noisrev oriac ,14.42.3 noisreV +KTG ,ung-xunil-cp-46_68x ,1 dliub( 05.0.13 scamE UNG"

Concatenating Emacs variable in Fennel:

(.. "Emacs version: " Elisp.var.emacs-version)

Loading the value as Emacs Lisp variable:

(require-fennel variable-example)

variable-example

Result:

"Emacs version: 31.0.50"

Evaluating arbitrary Emacs Lisp:

(string.upper (Elisp.eval
               (.. "(with-current-buffer \"*scratch*\""
                   "  (buffer-substring-no-properties (point-min) (point-max)))")))

Loading the result as a variable:

(require-fennel eval-example)

eval-example

Result:

";; THIS BUFFER IS FOR TEXT THAT IS NOT SAVED, AND FOR LISP EVALUATION.
;; TO CREATE A FILE, VISIT IT WITH ‘C-X C-F’ AND ENTER TEXT IN ITS BUFFER.

"

Passing functions to Fennel

Passing an Emacs Lisp function to Fennel:

(cljlib.mapv #'capitalize ["phil" "andrey" "alys" "emma" "alex"])

Result:

["Phil" "Andrey" "Alys" "Emma" "Alex"]

Passing an Emacs Lisp lambda to Fennel:

(cljlib.mapv (lambda (s) (capitalize (reverse s))) ["phil" "andrey" "alys" "emma" "alex"])

Result:

["Lihp" "Yerdna" "Syla" "Amme" "Xela"]

Questions?

Thanks!