Make a Lisp (mal) in Rust

written July 2018

I'm excited to tell you about my recently-completed project to Make a Lisp using the Rust programming language: mal-rust.

mal-rust hello world

There is already a great Rust implementation in the main mal repo, but I wanted to use this as an exercise to learn more about Rust, and so I purposely did not look at the already-completed implementation.[1]

Here I will document some of my experience and things I've learned in the process, and some tips if you are new to Rust too!

The borrow checker isn't as mean as I once thought.

I've toyed with Rust several times before, and it usually ended with my hands in the air, frustrated at the borrow checker. Also, every beginner Rust tutorial you read has a big warning about the borrow checker and its learning curve.

I don't know exactly what was different this time, other than maybe my own expectations. I went into the project thinking, even feeling that my success with Rust will depend on my understanding the borrow checker. And, in the end, I believe him and I came to a sort of understanding!

I came to appreciate the borrow checker as a friend with a few very simple rules. And I finally read up on Rc and RefCell, which allow one to write code that still satisfy those rules!

When the borrow checker and I couldn't come to terms, then Rc and clone() allowed me to satisfy him in a safe way. It's a nice dance, and I learned to love it.[2]

borrow checker error

Tip: Learn to appreciate the borrow checker's errors. They will help you understand ownership and lifetimes better than any tutorial can!

I still don't fully understand the lifetime syntax.

I've read numerous tutorials about Rust lifetimes, and I think I understand the concept. But I still struggle with the syntax for explicitly annotating lifetimes.

Good news is, I was able to finish the project without needing to annotate lifetimes at all. Well, I think that's good news.

Tip: I don't know if I should feel bad, but I'm currently of the opinion that annotating lifetimes is something that the Rust masters do. I'll update this document when I meet one (or become one, haha).

Rust's pattern matching is really cool.

I wish I had Rust's pattern matching ability in Ruby and JavaScript. That is all!

match syntax example

Tip: Rust's pattern matching can not only match on structs and enums, but nested levels of tuples and structs and enums! Give it a try!

Newtype is a pattern my OO inheritance-trained brain struggled with.

Rust doesn't have inheritance like every other language I've used. Instead, you are encouraged to use composition for enhancement. This was super weird for me.

newtype pattern

Tip: If, like me, composition is a pattern you don't use regularly, then expect to dedicate some time to struggle through it, like I did. It's worth it!

Cargo's built-in test runner is genius.

Friction for adding tests to your code is very low thanks to the test runner built into Cargo. While testing statically-typed Rust code isn't quite as critical as testing dynamically-typed Ruby code, testing still helped me catch and fix bugs quickly.

I set up an observr process in a split window to run cargo test && cargo build every time I saved a file. This made my red-green-refactor loop much tighter!

observr split window running tests

Tip: Run your tests automatically when you save a file. If you don't have an IDE that will do it, then you can gem install observr and use my script here.


I really enjoyed learning some Rust. I know I have a long way to go, but this was a good step.

Rust is a uniquely mind-bending language. It's good to have it in your tool belt, even if for the purpose of learning and appreciating the composition pattern, data ownership, and limiting shared mutable state.

mal is an amazing project, and I recommend you use mal as a starting point for learning any new programming language. It will force you to learn more of the language than any small project might, and the mal guide is detailed enough to eliminate a lot of wheel spinning about project architecture.[3]


  1. Well, I did sneak a peek near the end to figure out why my implementation was half-as-fast as that one. That problem has since been fixed!
  2. Yes, I know there is a cost to using Rc. But sometimes it's the only way, and that's what I learned in this project.
  3. Sure, you don't ever get bogged down in thinking should I put this in a bare function or in a class or should I break this up into two classes..., but I do.

Hosting for this site is provided by

The SDF Public Access UNIX System