transmutrix

Falling in Love With Lisp, Part 2

This is a continuation of Part 1. In that part, I talk about the series of events that led me to finally trying out a Lisp, specifically Fennel. In this part, I will write about my experience learning the language, and writing a game from scratch with it.

Loose Ends

First, I want to briefly tie up an idea I touched on in the first part but didn't fully articulate: I don't like where the software industry is going, broadly. I don't want to participate, or perform any of the tolerance or excitement about the massive deskilling and enclosure of yet more commons that is necessary to be persona grata in the corporate software world any more.

In this sense, I guess you could say I'm a programmer going my own way, whatever that comes to mean.

I love computers and I love programming, but insofar as it's something I do for money, I'm only doing it insofar as I agree with it. I refuse to ever again be the person tee-heeing about my overpaid tech job at the Torment Nexus Factory. At the first whiff of bullshit, I'm out, and if that means it takes me a while to find a place I belong, if that means I have to put the fries in the bag, then fine.

People who love computers, really love them, have an opportunity at this point in time to do an "inverted Atlas Shrugged" where we deliberately put our collective feet down and stop being footsoldiers to money-worshipping bullshit, and spend our time doing things that are actually useful, even if not always profitable.

I refuse to pretend to be a good little worker bee for an economy that's dominated by a tiny group of assholes gambling and running scams. I intend to make things that are interesting and good, and damn the rest of it.

Now, with that out of the way, let's talk about Fennel and Priiism!

Starting the Project

This project was begun with gusto on Sunday, August 30, 2026. I came in from my walk and wrote down all our ideas for priiism, then set up a Fennel REPL in LÖVE by reading through this template project and borrowing a few files from it.

I could have cloned the repo and filled out the config files, etc. but I didn't want to do any of that. When I'm learning a new thing, especially a new language, I want to do everything as from-scratch as I can tolerate, because I'll learn more. So I borrowed a few files and edited them to my preferences.

I was so excited about having the REPL set up that I went out into the night and had a little jog. This is significant because I'm a fat girl and I don't go out for jogs. It was nice. But I need a good sports bra these days.

I stayed up late that night working more on the game, and getting a feel for writing the basic syntax of Fennel. It's my first Lisp, after all. The first thing I did is set up a tweaks file (called g/config.fnl in this codebase) and a small utility to make a "safe zone" fitted to the game window, but allowing for overdraw.

The stuff I did each day is detailed in a diary.md in the repo for the game which I plan to release at some future point, and I don't know how valuable it is to summarize it all here.

In summary: With a REPL and ,reload <module> I was off to the races, tried to keep the project architecture sane and simple, and that carried me through to the end.

LDtk

I am not a proficient user of LDtk. This is the first time I've seriously used it on a project, actually. I've been a Tiled user for a very long time, and I've dabbled with alternatives like Ogmo as well.

I think LDtk would have served me best if I had taken time early to set up fancy autotiling rules from the start. As I used it, I encountered difficulties and spent a lot of time deciding how to do Foo in LDtk, how to parse Bar from LDtk, how to express Baz in its entity fields system, etc.

Some elements of LDtk's UX are very ahead of the competition, and others baffled me. Some of this was user error but some was just really confusing behavior of the editor. I'm eager to explore the capabilities of LDtk more fully now that I'm not on a time crunch.

Writing a loader for LDtk files was surprisingly straightforward! I used the GOAT, json.lua to parse the files, then wrote my loader in Fennel! It's under 300 lines, but it's tailor-made for this game, and doesn't support every LDtk feature. I wrote it piece by piece by using fennel.view, reading the official docs and my LDtk json files themselves as I worked.

If other people really want my loader, I could fill it out more and make it available, but honestly, for anything under 1000 lines of code it's been my experience that a reference implementation that you don't directly use is often better than a library.

Core Architecture

I was unaware of tiny-ecs and similar projects when I started on this game, and was eager to learn Fennel by doing anyway, so I created the simplest possible entity component system I could.

My source code is organized like this:

g/
├── ldtk.fnl
├── util.fnl
├── config.fnl
├── state.fnl
├── entity.fnl
├── ...
├── modes/
│   ├── mode-error.fnl
│   ├── mode-intro.fnl
│   └── mode-start.fnl
├── cutscenes/
│   ├── common.fnl
│   ├── fall-down.fnl
│   └── ...
├── components.fnl
├── components/
│   ├── transform.fnl
│   ├── collector.fnl
│   └── ...
├── prefabs.fnl
├── prefabs/
│   ├── player.fnl
│   ├── sunflower.fnl
│   └── ...
├── systems.fnl
└── systems/
    ├── _template.fnl
    ├── textures.fnl
    ├── tile-occupants.fnl
    └── ...

In this scheme, here's how the game breaks down:

A component is a function that takes no arguments and returns a plain table with default values, like this:

; g/components/collector.fnl
(lambda [] {
 :color :White
 :send-on-complete ""

 ; runtime state
 :hit-state     :none ; :none :wrong :satisfied (later: partial?)
 :old-hit-state :none
 :time-state-changed 0})

A prefab corresponds to an LDtk entity type, and it takes an entity along with the loaded LDtk data for it, adds components to the entity, and overrides the component fields it wants, like this:

; g/prefabs/mirror.fnl
(local entity (require :g.entity))
(local mirror (require :g.mirror))
(local components (require :g.components))

(fn [ent data]
  (components.add ent :transform {
    :x data.spawn-x
    :y data.spawn-y})
  (components.add ent :sprite {
    :path (mirror.pick-sprite data.fields.Angle)
    :off-x -16
    :off-y -24
    })
  (components.add ent :signals)
  (components.add ent :pushable {
    :lock-horizontal (not data.fields.PushableHorizontal)
    :lock-vertical   (not data.fields.PushableVertical)
    :track-iid (or (and data.fields.Track data.fields.Track.ent-iid) "")
    })
  (components.add ent :bonkable)
  (components.add ent :tile-occupant)
  (components.add ent :mirror {:angle data.fields.Angle})
  ent)

components.add actually checks to make sure that you aren't adding new fields to the component, since that's usually a mistake. It does however mean that components often contain the empty string or another default value in cases where nil would, absent this field check, otherwise be fine.

I still allow adding fields to components outside of this call, there's no metatable to enforce the rule outside construction, but in general this has been a good guardrail.

Finally, we have systems, where all of the gameplay code actually lives. Each system is a module registered in an author-controlled execution order. It can have hooks the systems module calls into, but it can also expose utility functions or data.

There's a system to draw sprites, a system to handle fading things out, a system to handle player movement, etc.

Not only top-level gameplay code lives in systems: Basically everything that isn't reusable, anything that runs at a given time during a frame, is in a system. The texture cache is a system, the audio manager is a system, there's a system that runs the GC, and another that profiles all the systems' runtime.

Here's one example of a system:

;; trigger music once when the player overlaps a certain region

(local save (require :g.save))
(local audio (require :g.systems.audio))
(local entities (require :g.systems.entities))
(local M {})

;; -- helpers ---------------------------------------------
(fn find-player? []
 (var player? nil)
 (each [ent (entities.each-with-comps :transform :controllable) &until player?]
  (when (not ent.controllable.disabled)
        (set player? ent)))
 player?)

(fn on-trigger? [trigger player]
 (and (>= player.transform.x trigger.rect.x)
      (< player.transform.x (+ trigger.rect.x trigger.rect.w))
      (>= player.transform.y trigger.rect.y)
      (< player.transform.y (+ trigger.rect.y trigger.rect.h))))

;; -- system calls ----------------------------------------
(fn M.sys-tick []
 (let [player (find-player?)]
  (when player
   (each [ent (entities.each-with-comps :rect :music-trigger)]
    (when (on-trigger? ent player)
     (print "[music-triggers] playing" ent.music-trigger.path)
     (audio.play-stream ent.music-trigger.path)
     (save.record-true ent)
     (set ent.music-trigger nil))))))

;; -- module exports --------------------------------------
M

Top level utility libraries are directly under g/. Everything else is neatly categorized, so I could easily do ,reload g.systems.beams in the REPL. I didn't set up the Fennel LSP, I just organized my project in such a way that it's very easy to find things.

Components, prefabs, and systems each have a "registry file". Components are registered in a hash of factories, prefabs are the same but for building whole entities, and systems are arranged deliberately in execution order.

My ECS design here is based in part on time I've spent working on the Pixel Washer codebase as a freelancer. I'm responsible for the pathfinding and a swathe of performance-related changes across various systems there, but the elegance and convenience of Matt's ECS setup struck me as beautiful, and I've aped a few aspects of its core design on a couple of my projects.

This setup, combined with the joy of writing Fennel, meant that most things I needed to add to the game were fairly straightforward to add with minimal boilerplate, and almost all my time was spent solving the real problems I needed to solve. I spent exactly zero time on this project bemoaning a language limitation or how boilerplatey something was to write.

The architecture of this game is actually very "plain" and straightforward, so I don't want to belabor it too much. In general I try to shape my codebases such that it's easy to grep your way around and find things, whether fancypants language tools are available to you or not.

Growing Pains

Honestly, learning Fennel has been a fairly smooth experience. I had a few hiccups early on: I installed RainbowBrackets because sometimes I rearranged code lines only to mess up the scope something was at. Honestly, just having colored brackets alone made Lisp much easier for me to read and write, it's a huge leg up.

One risk is accidentally moving some code outside a function, but it happens to run harmlessly at load time, so you don't immediately notice.

Another mistake was thinking #foo would be equivalent to (length foo) - it is not! From the Reference (linked below in the Resources section):

#val ; same as (fn [] val)

The hash operator (#) in Fennel is used to create anonymous functions in a shorthand form, useful for passing small functions inline.

Similarly, it took me a little while to habitually use icollect and accumulate instead of manually writing loops to build the result all over the place. Embarrassingly, I didn't realize at first that you could do this: (. foo bar baz) instead of (. (. foo bar) baz) ( both are equivalent to foo[bar][baz] in plain Lua).

There are some Fennel features I haven't even touched yet, like partial, match, and case-try. Actually, until writing this article, I had missed the ?. operator which would have been very handy in some of the code I wrote for this game! During development I even thought about adding this operator myself with a macro, but didn't. I've used macros a little bit, but only in a few limited cases, like adding a += operator.

There are definitely still places in the game where there is a much more elegant solution than what I wrote, but even my clunky "C in Fennel"-style code is terse and sometimes, dare I say, even beautiful.

For the most part, I just wrote straightforward Lua code in the Fennel syntax, using forms where it seemed appropriate, and the result was very readable code with a reduced baseline level of hoopla, and the compiler always provided me really good error messages when I did anything silly.

Now that the pressure of the grant deadline is off, it may be nice to go through the codebase and idiomaticize my code in places, and learn some stronger Fennel-fu.

Deadlines and Confounders

I had 4 weeks to make this game prototype, and for about 3 of those weeks I was house sitting at a hobby farm, looking after goats and chickens! I enjoyed it a lot, and the morning-and-night routine of caring for the animals paired well with programming all day in the quiet between chores.

I dragged my Mac Mini and a keyboard and mouse over there, and other than chores and errands, of which there were many, I spent a LOT of time programming.

I did have to take a couple days to rest here and there, because I managed to get a short-lived illness not once, but twice, while there.

The first time, both I and my partner got sick for about a day, seemingly a stomach virus or something? We doubted food poisoning because we hadn't eaten any of the same foods, except for string cheese. The second time, I was there alone and got food poisoning from a bad slice of boloney at the bottom of the pack.

We also had some visits with my partner's mom during the stay, and my own mother's birthday was in the middle, so I went out and spent time with her.

Apart from all that stuff, I'm just very tired of cramming for deadlines. I've basically been doing it in some form, without and substantial breaks, since... 2020? I need to find a more sustainable rhythm of work.

Alas, as a Bipolar [Lisp] Programmer it's hard to find that balance. I'm either On and 100% focused on something or I'm entirely disinterested and/or mega depressed. At least, that's how it is if I eat a normal diet with a normal amount of carbs. I love carbs. -n- On keto I seem to be slightly manic most of the time, which is... Arguably an improvement? The jury is out, check in with me in a year. Feels like cheating somehow and also I really, really miss carbs.

Despite exhaustion from working non-stop on this for several weeks straight, I had a couple days left at home after the farming sojourn ended, and managed to squeak out some puzzles, cutscenes, and submit the game to the grant folks. There are over 400 other applicants, so who knows if we'll be selected or not! Either way this experience has been a lot of fun and has taught me a lot.

Beginning to Develop "Lisp Brain"

At some point learning a new language you begin to "get it" and things become effortless and intuitive to you. I'm no expert on Fennel yet, but already I have felt a shift in how I write and how I think.

Lua itself suited me well for some of the same reasons that C did: It's a small language with a limited number of things in it, and you can remember most or all of them, and then construct whatever you want out of those uniform pieces. Lua does a better job of this than C, in my view.

People would say that Lua has "high orthogonality" because it gives you a tiny number of primitives you can combine to create anything.

Lisps take that feeling to a whole new level. Not only is there a tiny set of language primitives, but the expressivity is huge. When I first dabbled in MoonScript, it was the huge upgrade in expressivity over Lua that drew me in and made me into a long term enthusiast.

Fennel offers me a similar boost, and because Fennel is a Lisp, I can add any silly construct from MoonScript, or Ruby, or my brain if I choose, like unless:

(macro unless [cond result]
 `(when (not ,cond) (do ,result)))

(local foo false)
(unless foo (print "hello"))

Which compiles to:

local foo = false
if not foo then
  return print("hello")
else
  return nil
end

The only thing I'm not sure about yet is how to implicitly make available a bunch of language constructs like that in many dependant files without doing an import-macros and locally binding every macro in every user module. That is, how to make Fennel pretend your forms are part of the base language, like using #lang in Racket.

Fennel, like MoonScript, also has some "sane defaults" built in to offer benefits over Lua, like local by default, tail position returns, etc. Its design choices help to elide bugs and superfluous typing.

Affordances vs Prescriptivism

One reason I tend to write my own game engines, using libraries or lightweight frameworks like LÖVE, is because I don't like that off-the-shelf engines uniformly bring with them a kind of architectural prescriptivism.

When evaluating a tool, I am very aware of what affordances it offers me and what restrictions are placed upon me in return, or what effort I'll be required to give in order to benefit. In general, I'm a very industrious programmer, but I'm hard-headed and ornery, and I don't have much patience for things being overtly shitty without good reason.

I tend to prefer writing my own engine per-game because each game is solving different problems, and at the scale of indie games, it's often less work to write the engine than it would be to contort every problem into the one allowed shape of an Unreal Game, or a Unity Game, etc.

It's also true that my ease of working in a codebase is proportional to my understanding of the codebase. Godot is probably the closest thing to my preference because the source code is reasonably sized and easy to read, but I still tire of "figure out how everything in your game is a Node and use our programming language and use our IDE and-".

This feeling of being penned in and forced to contort things into a shape handed down from on high, instead of just focusing on solving the problem itself, doesn't just apply to game engines. It also applies to jobs, but more importantly: it applies to programming languages.

Programming languages love to tell you what control flow structures are allowed, and iff you are able to add new ones it is frequently a hack and frowned upon. Languages love to separate statements and expressions. Languages love to lack basic reflection, or provide half-baked language features to make up for things that are just missing.

But don't worry, they say. You can write all your C in a specific way so that you can write a parser to read your C and then generate serde code for your structs, it's fine. Don't worry, you can develop a new format to specify your datatypes in and then parse that to generate the C code.

I've done all those things and it isn't a good solution in my estimation. It isn't worth the long term hassle and I don't think continually papering over things being sucky with machismo and hand-waves that nothing better is available is a good solution either.

This is where the large, long-term benefit of Lisps lies. Lisp is not prescriptive. Lisps impose very little on you, and leave you free to expand the language as needed to fit whatever shape you would like. Of course I would like Lisp, I'm the compulsive jerkass who writes a new game engine for every project she has!

Fennel now joins Lua, MoonScript, Nim, ES6, and C in my pool of particularly-favored languages (near the top, with MoonScript and Nim). For now I'm taking a short breather, but I'm excited to do more with Lisps in the future.

Resources I Recommend

Priiism

You can check out the demo here if you like!

Stats at the time of writing: