A love letter to s7 Scheme
One of the main technical goals I set for Galactology was to fully support user mods. The core of the game is written in C++, and asking for mods to be delivered as native DLLs is a mess and also non-portable, so it was clear to me early on that the best would be to enhance the existing Scheme scripting support, to the point I wanted to use it instead of C++, and then rewrite most of the game in Scheme, as if it was collection of mods.
Galactology, as the sequel slash rewrite of The Spatials, inherited its support for s7 Scheme. But what was there had a very limited scope. It was only able to be run for scripting planet missions, and it had limited support for interacting with the rest of the C++ side.
Why s7 Scheme?
My limited knowledge of Scheme at the start of Galactology development didn’t go very deep into understanding s7. I just integrated s7 as an (immensely) faster interpreter to replace TinyScheme. My Scheme efforts were limited and conservative. A few macros, tentative use of a few data structures, not much. This was in part because of the limited feature set of TinyScheme, and because I hadn’t really started to look into the s7 features, rather only on its C FFI API for embedding, which is excellent and very natural to work with, given it’s the main focus of the interpreter.
Applicative syntax everywhere
So I started reading on how Scheme deals with aggregate data. It looked like a good point to start. Galactology was going to be written in a big part in s7 Scheme so data modeling was a required topic of investigation.
Keep in mind, at this point, I had written less than 2K lines of scheme in my life, so I was still quite a newbie, and a blank slate. I was familiar with association lists, but surely there had to be some other patterns. I learned about vectors and vector-ref
, and then about SRFI-69 hashtables. But APIs like hash-table-set!
didn’t look very nice to me. Such a very special cased/named procedure, it looked out of place in the language.
Kind of bummed, I dived back into the s7 documentation, to see how these were exposed in s7. And then it clicked for me:
> (define h (hash-table* 'a 1 'b 2))
> (display (h 'a))
1
> (set! (h 'b) 3)
> (display (h 'b))
3
Eureka! What was this wonder? This was my first encounter with generic applicative syntax, which s7 embraces fully in many places for many kinds of objects. It’s not limited to hash tables:
> ; lists
> (display ('(1 2 3) 1))
2
> ; vectors (with constant time)
> (display (#(1 2 3) 1))
2
> ; strings
> (display ("abc" 1))
#\b
> ; environments (!?!)
> (display ((let ((a 2)) (curlet)) 'a))
2
> ; dilambdas (more on this later)
> (define dil (let ((a 0))
(dilambda
(lambda () a)
(lambda (v) (set! a v)))))
> (display (dil))
0
> (set! (dil) 10)
> (display (dil))
10
And, as you can see, if an applicative reference form (a b)
is valid, you can usually (set! (a b) ...)
too.
It turns out than s7 really embraces its interpreted, dynamic nature. Many of the Scheme specs are written to not make the life of static compiler writers too hard, but that’s not a problem for s7. It supports a good chunk of standard Scheme, but it revels on the parts that are its own, and the result is a more usable language IMHO.
Reactive symbol access
For a few years already React and friends have put reactive programming on the map. But when the language is truly dynamic we can go even further than what the original Javascript versions that React targeted. What if we could get custom code called when any let slot is accessed? Enter symbol-access
:
> (define a 1)
> (set! (symbol-access 'a)
(lambda (s v)
(format #t "hey, changing ~A into ~A\n" s v)
v))
> a
1
> (set! a 2)
hey, changing a into 2
> a
2
We could manipulate the value of v
before returning it, or call any external side effects, or mess with the environment if we wanted. It’s not just for accessing slots of simple values, it can also be implemented to support drilling down into more complex objects as described in the reactive-let
documentation (see the previous link for symbol-access
).
Generic iteration
Many s7 built-ins can iterate generically over different data types. For example map, for-each, or reverse. This means you can pass not only lists to them, as the Scheme standard requires, but also vectors, hash tables, or even environments.
First class environments
This is when I truly realized how crazily dynamic s7 is. Other interpreted languages can manipulate their environments, but I’ve never seen anything like this:
> (define in (let ((a 1)) (curlet)))
> (in 'a)
1
> (set! (in 'a) 2)
> (in 'a)
2
> (define f (with-let in (lambda () a)))
> (f)
2
> a
error: a: unbound variable
The full API is documented here and it’s an enlightening read. By reading this deconstruction of what is an environment in s7 I finally understood the exact semantics of symbol lookup in Scheme, and how environments in dynamic languages work in general.
You can build a class system based on this very flexible handling of environments alone, and indeed s7 provides one at the end of that documentation section.
(values) is just splice-in-place (mostly)
values
is the constructor for the multiple return values support in standard Scheme. But in s7 it is a bit different, and it allows to splice lists directly as syntax inside an expression:
> (define l '(1 2))
> (+ (apply values l))
3
But, for me, its most useful property is a special case it has just for map
. It allows to skip certain values when mapping over a sequence:
> (define s '(1 2 3 4 5 6 7 8 9))
> (map (lambda (v) (if (odd? v) v (values))) s)
(1 3 5 7 9)
Conclusion
s7 Scheme feels like a gateway into more advanced Lisp-like languages, a step up from the mini-Schemes, but at the same time it has unique features not found in the big Schemes or Lisps, which are elegantly implemented, and following a clear vision of programmer ergonomics and dynamism.