Fun with Lisp string interpolation

May 31, 2026

String interpolation is a programming language capability where the results of inline code are spliced into strings. It makes for very convenient construction of strings and messages.

Many languages can do this. For example, Python has string interpolation with built-in format directives:

import math

print(f"Pi divided by 2 is {math.pi / 2:.4f}")
Pi divided by 2 is 1.5708

You can do the same thing in other languages, with varying degrees of flexibility. For example, some languages will have interpolation, but no built-in formatting shorthand.

Here's Bash. I'm using bc for math and printf for formatting, but the point is it too can evaluate and substitute code within strings:

pi=$(echo "4*a(1)" | bc -l)
echo "Pi divided by 2 is $(printf "%.4f" ""$(echo "$pi/2" | bc -l))"
Pi divided by 2 is 1.5708

I personally like Python's syntax for this1. I find {<code>:<fmt>} quite easy to parse. A bit ago, I set out to implement something like this for Common Lisp (CL). We do have cl-interpol, but it's a rather complex library with some limitations for inline formatting.

The result was f-string, a simple Common Lisp interpolation libary I'm quite proud of! I'll spare you most of the details here, but please read the linked page if you're interested. The bottom line is that it uses a custom CL reader macro to parse and convert the string, with syntax that's Python-adjacent and using the built in format for formatting under the hood:

(write-line #f"Pi divided by 2 is {(/ pi 2);~,4f}")
Pi divided by 2 is 1.5708

Common Lisp is fun like that: you can just define your own syntax at the language reader level. I've also ported this library to CHICKEN Scheme, though it's still a work in progress.

More recently, I've been getting back into Emacs Lisp developemnt. I thought it might nice to have something similar in elisp. The s.el library can do some interpolation, but to my knowledge we don't have anything quite as ergonomic as Python's f-strings. One minor snag is that Emacs Lisp doesn't have reader macros, so we have to use the usual parenthesized Lispy macro to achieve this.

The fun thing here is that the implementation from Common lisp actually mostly translated over! Emacs doesn't quite have streams in the CL sense, but it has certain funtions that can treat buffers as output streams. We can treat a string as a simple input stream by simply advancing a start pointer into the string and updating the variable pointing to it.

After a couple of shim functions and a little frustrated keyboard mashing, we achieve fstr:

(princ (fstr "Pi divided by 2 is {(/ pi 2);%.4f}\n"))
Pi divided by 2 is 1.5708

Kinda neat. Might never use it.

Footnotes:

1

…​he said, ducking tomatoes hurled by fellow Lispers.