Fennel
Yesterday, System Crafters did a live episode on Fennel, a Clojure-inspired Lisp that compiles to Lua. What fun!
Fennel is easy to install because it's just one file. Here's what I did on my Debian 12 box.
Lua
If you don't already have Lua installed, start with that. There isn't a Debian package called lua
, there are several different versions. The latest is called lua5.4.
sudo apt install lua5.4
The resulting executable is called lua
.
❯ which lua
/usr/bin/lua
❯ lua -v
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
We'll want lua-mode
for Emacs.
M-x package-install RET lua-mode RET
There is a Lua Language Server too. We'll compile that from source.
sudo apt install ninja-build
git clone https://github.com/LuaLS/lua-language-server
cd lua-language-server
./make.sh
The Lua language server expects to find files in the current working directory, so instead of adding a symbolic link to it from a convenient bin directory in your path, make a wrapper script.
❯ cat ~/.local/bin/lua-language-server
#!/bin/bash
exec "/home/tim/lua/lua-language-server/bin/lua-language-server" "$@"
In Emacs, eglot
will find the Lua language server when invoked from a Lua file.
❯ cat ~/.emacs.d/config/programming/lua.el
(add-hook 'lua-mode-hook 'eglot-ensure)
Now eglot
will find it whenever we open a Lua file.
Fennel
Once we have Lua, Fennel is easy to install because it's just one additional file!
mkdir fennel
cd fennel
wget https://fennel-lang.org/downloads/fennel-1.4.0
chmod +x fennel-1.4.0
We can check the signature too.
wget https://fennel-lang.org/downloads/fennel-1.4.0.asc
curl https://technomancy.us/8F2C85FFC1EBC016A3B683DE8BD38C28CCFD2DA6.txt | gpg --import -
gpg --verify fennel-1.4.0.asc
Now create a symbolic link to it from a convenient bin directory in your path
ln -s ~/fennel/fennel-1.4.0 ~/.local/bin/fennel
If we start the Fennel REPL now, it suggests installing readline.lua
from luarocks. But Debian provides readline.lua
in package lua-readline
sudo apt install lua-readline
We may wish to install LuaRocks in the future, but this is fine for now. Running fennel
gives us the REPL with GNU readline functionality.
❯ fennel
Welcome to Fennel 1.4.0 on PUC Lua 5.4!
Use ,help to see available commands.
>> (+ 1 2 3)
6
We can persist our history with a .fennelrc
file.
❯ cat ~/.fennelrc
; persist repl history
(match package.loaded.readline
rl (rl.set_options {:histfile "~/.fennel_history" ; default:"" (don't save)
:keeplines 10_000})) ; default:1000
There's a fennel-mode
for Emacs.
M-x package-install RET fennel-mode RET
But I'm not aware of language server for it.
Update: There is a Fennel Language Server! Thanks, @technomancy@hey.hagelb.org!
git clone https://git.sr.ht/~xerool/fennel-ls
cd fennel-ls
make
make install PREFIX=$HOME
Well, that was easy! Now configure Emacs to use it whenever we open a Fennel file. Also, since Fennel is a Lisp, we'll turn on parinfer
for it.
❯ cat ~/.emacs.d/config/programming/fennel.el
(add-hook 'fennel-mode-hook 'parinfer-rust-mode)
(add-hook 'fennel-mode-hook 'eglot-ensure)
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs '(fennel-mode . ("fennel-ls"))))
And away we go! Here's fizzbuzz in Fennel.
Mixing Fennel and Lua
Just as we can use any Java library from Clojure, we can use any Lua library from Fennel. In particular, there is a terrific Lua library called Lume. It has all kinds of things that didn't make it into the Lua standard library, but maybe should have.
(local lume (require :lib.lume))
(print (string.format "uuid: %s" (lume.uuid)))
The Lume README.md
contains this lovely example, which gave me a chuckle (Oh hi mark).
(print (lume.format "{b} hi {a}" {:a "mark" :b "Oh"})) ;; Oh hi mark
I even created my own library by copying the pairsByKeys
function from Programming in Lua and calling it from Fennel. Here is the Lua library file (cat lib/sorted.lua
)
local sorted = {}
function sorted.bykeys (t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0 -- iterator variable
local iter = function () -- iterator function
i = i + 1
if a[i] == nil then return nil
else return a[i], t[a[i]]
end
end
return iter
end
return sorted
and here is some Fennel that calls it (cat main.fnl
)
#!/usr/bin/env fennel
(local sorted (require :lib.sorted))
(let [start-clock (os.clock)
current-epoch (os.time)
current-datestring (os.date)
current-table (os.date "*t")]
(print (string.format "\ncurrent epoch: %s" current-epoch))
(print (string.format "\ncurrent date: %s" current-datestring))
(each [k v (sorted.bykeys current-table)]
(print (string.format " %s: %s" k v)))
(print (string.format "\nelapsed time: %.5f\n" (- (os.clock) start-clock))))
Running it gives
❯ ./main.fnl
current epoch: 1704562499
current date: Sat Jan 6 12:34:59 2024
day: 6
hour: 12
isdst: false
min: 34
month: 1
sec: 59
wday: 7
yday: 6
year: 2024
elapsed time: 0.00006
Note that the date components (day, hour, &c.) are in lexicographical order. If we had iterated over pairs
instead of sorted.bykeys
, they would have come out in more or less random order.
Update: I just noticed Hugo isn't rendering the colors for fennel-mode on gitlab. It looks fine locally. Here is a screenshot of my Emacs frame.