Lispier Rust with Generics
In Lisp, as in languages like Python, there are no types. No, wait, there is just one type, and everything is that type. No, that’s not right either. Well, whatever the types really are, it certainly feels from the programmer’s perspective like there are no types, because types are never or only rarely declared. C is not like this, because in C you’re constantly declaring types and casting and so forth.
In the C core of Emacs, an elaborate system of macros makes it possible to write C code that feels like writing Lisp, or at least some kind of dynamically typed language. Remacs is an effort to rewrite the C core in Rust. Rust allows for generic types, which aren’t available in C, and using these it’s possible to make the low-level code even more Lispier than ever.
Here’s some concretely-typed Rust1. cons
is defined in C, and is exported to Rust as Fcons
. LispObject::cons
is a wrapper around the unsafe Fcons
, and both functions take two LispObjects
and return another one. frame_position
is defined in Rust, where it takes a LispFrameRef
as an argument and returns a LispObject
. It is wrapped with the lisp_fn
procedural macro, which will cause it to be exported as a function called Fframe_position
which takes LispObject
for its input and output.
frame_position
just returns a cons pair containing two fields from the given frame. But those fields are numbers (c_int
, specifically), and LispObject::cons
requires LispObjects
, and so the numbers must be cast before passing them along.
Explicit type conversion of this sort was happening all over the place in the Remacs Rust code, and it was ugly and burdensome. Wouldn’t it be easier to just skip it? Well, generics make that possible.
Rather than forcing the caller to cast the arguments in preparation for consing, we can simply tell LispObject::cons
to take care of the casting itself, provided that the arguments are of a type that can be converted to LispObject
2:
Isn’t that nice? Now LispObject::cons
takes two arguments, each of which is something that can be converted to LispObject
, and those arguments are converted before being passed along to Fcons
. The caller doesn’t need to worry about types anymore!
This is not bad, and the Rust LispObject::cons(4, "abc")
looks a lot like the Lisp (cons 4 "abc")
. But in Lisp it’s also common to use literal syntax like '(4 . "abc")
, without calling a function. Can this be done in Rust? Yes, by implimenting tuple conversion for LispObject
:
Now frame_position
doesn’t call LispObject::cons
at all – it just uses a native Rust tuple! And because it no longer returns a LispObject
, its return type can be updated to the more specific (c_int, c_int)
. Because a tuple of things that can be converted to LispObject
can itself be converted to LispObject
, the lisp_fn
macro will ensure that this function is still properly exported as a Lisp function with LispObject
inputs and outputs.
Note that because the From
and Into
traits are defined in terms of each other, the definition for tuple conversion is recursive, and so it can handle arbitrarily nested tuple pairs “for free”.
Exercise for the reader
It’s possible to convert arbitrarily nested tuple pairs into a LispObject
. Is it possible to destructure a LispObject
into arbitrarily nested tuple pairs?
Remacs PRs containing further commentary
- Make cons generic
- impl From<(S, T)> for LispObject
- Add tuple conversions
- Use impl notation for generics
Footnotes
1 Irrelevant and boring details have been suppressed.
2 This implementation uses the impl Trait
syntax, new in Rust 2018. Without that notation, LispObject::cons
would be more verbose:
impl Trait
is unpopular with some, but I love it. Why should I need to come up for names for the types when the names aren’t used? We have anonymous functions, why not anonymous types?