require-fennel.el
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!