← All posts

Chez from a Clojurian perspective

December last year, I started working on this rich text editor. I had experience in Clojure but its startup time wasn't ideal for a text editor. Common Lisp and Chez Scheme fit that requirement. I wanted to learn about continuations and hygienic macros so I chose Chez Scheme. This is a 1-year review.

Syntax

Scheme uses lists for almost everything. Even with experience in Lisp, I felt "so many parentheses" for a couple of weeks.

To this day, I miss literals for data structures like vectors, maps, sets, anonymous functions, etc. The most important benefit of vector syntax is that it doesn't evaluate; thus you don't need to quote or unquote data. Same for maps and sets. Individually these things don't matter much but together they add up.

ClojureChez Scheme
[1 a 3]
`(1 ,a 3)
(list 1 a 3)
{:a 1
 :b (something arg)}
`((a . 1)
  (b . ,(something arg)))
or
(hashmap 'a 1
         'b (something arg))
#(somefunc %)
(lambda (arg)
  (somefunc arg))

Additionally, Scheme uses explicit grouping of bind pairs in let expressions. Initially, it feels more noisy but you quickly get used to it.

(let ([x 1]
      [y 2])
  (+ x y))

Brackets are used for visual distinction; otherwise programmatically they are the same as parentheses. Explicit grouping comes in handy while writing macros and you can embed non-binding forms in let expressions.

(and-let* ([a 1]
           [(valid? a)])
  (do-something a))

In Clojure you would probably write something like this

(and-let* [a     1
           :when (valid? a)]
  (do-something a))

Extra parentheses aren't a big deal since you can write your own macro but I wish Chez Scheme either had reader macros or had literals for data structures.

Polymorphism

Polymorphism is one of the core abstractions in Clojure. It makes many kinds of data structures and functions work together. You can use get-in to get data from inside a map containing a vector of maps. You can iterate through lists, vectors, sets, etc. in almost the same way. In Chez, you use data-specific functions like map, vector-map, list-ref, vector-ref, etc. or convert data types with vector->list, hashtable-keys, etc. This becomes inconvenient when iterating over mixed data types. The other handy thing about Clojure is that you can treat keywords and data structures as functions. This makes your program succinct.

On the other hand, lack of generic data structures and functions encourages you to make records for cases where you use maps, and create domain-specific functions. In Clojure you treat your program as a data processing pipeline whereas in Chez you talk more in terms of domain entities.

Macros

There are two kinds of macros: syntax-rules and syntax-case. Syntax rules is super easy to grok even for people who have never seen a macro and would be sufficient for the majority of cases. Syntax case is the general-purpose macro. It is harder to understand partially because of terse documentation and partially because you first need to understand phases, syntax objects, transformers, and datums.

Chez lets you create local macros with let-syntax which behaves just like let but for macros. You can do something similar with macro-let in Clojure but let-syntax feels more native and goes well with everything else.

Then there are more advanced things like compile-time properties and compile-time values. These come in handy when you are writing some large macros. You can achieve similar results using metadata on vars in Clojure. Scheme emphasizes separation of compile time and runtime while Clojure intentionally eliminates the distinction between the two.

Scheme advertises itself for hygienic macros but I never had a hygiene problem in Clojure. I think a more compelling sales pitch for Scheme macros might be pattern matching. Pattern matching goes well with code. It makes a macro's input and output more obvious.

If I had written this comparison at the 3-month mark, I would have called Clojure's macros better as they are easier to understand but now I feel a lot more comfortable writing large and complex macros in Scheme than in Clojure. You don't often write large macros but I think when used judiciously you get the most benefit of macros from these.

Scheme macros are more powerful once you get the hang of them.

Continuation

Before picking up Scheme, I was most excited to learn about this. It was the primary reason I picked up Scheme instead of Common Lisp.

Continuations are very powerful things. You can suspend the computation and resume later, resume multiple times, or modify something and resume again. But in my app, I found only two use-cases of them where their use was justified: co-operative threads and early return.

There are more applications of them though. For example, Chez doesn't have exceptions but uses conditions which are more general-purpose and powerful. You can raise a condition and let the handler decide how to continue from the exception without losing the call stack, raise a warning and continue ahead, or provide restarts to the handler to let it decide what to do.

In the current version of the app, I am not using the condition with continuation anywhere for two reasons:

  • I have always tried to eliminate exceptions in my program. It makes the program harder to read.

  • I had used it at a couple of places. But because I controlled most of the implementation, I refactored it to not need continuation.

But if someone were a library author and didn't have all the context to make a decision, they could raise a condition and let the application writer handle it appropriately. Or if the condition is many levels deep and passing down all the relevant data to make a decision would be either wrong or cumbersome, these can come in handy. Conditions introduce the same level of complexity as exceptions do in other languages but are more powerful constructs.

Interactive development

Here is a controversial take:
If Chez Scheme can say that it supports interactive development then Python can too.

This is quite disappointing because before starting the project I thought it would be more interactive than Clojure. The fundamental requirement for interactive development is that you can jump anywhere in the codebase, fix a bug, add a feature, eval, and iterate until you are satisfied. Then continue on to the next thing.

But in Scheme it does not work out of the box because modules and libraries are immutable. Only top-level definitions are mutable. The expected workflow is that you create a script-like file, add functions and macros, debug it, then wrap it into a module/library and develop the next layer. But I don't work like that. I make a module and immediately start using it. Then later on I add functions, debug it, modify it, and move things from one module to another. Another limiting feature of modules/libraries is that you can't inspect private functions. For this app, I mostly settled on:

  • Add/modify a feature

  • add test

  • run from cli

  • repeat

A much more interactive environment is possible. Chez Scheme has an interaction environment which is mutable. You can implement a namespace-like thing on top of it and together with continuations it can surpass the interactivity of Clojure. Currently, on an exception, you can walk the call stack and inspect variables in each frame. If paired with better interactivity, this can become a very enjoyable development environment.

Libraries

Libraries are a big deal for any project. You don't want to implement everything from scratch. Chez Scheme has maybe 200 libraries and most of them aren't maintained either. But this isn't a big problem because Chez has very good FFI with C. Most of the libraries you need will be available for C.

I am using GTK seamlessly from Chez, even using GObject inheritance to build custom widgets for Aksh. Great FFI and advanced features of scheme macros makes interoping with GTK and C quite convenient. If not, almost every language provides an easy way to define a C-compatible function. If you need a Rust or Swift library, just wrap it in C and call from Chez.

Summary

Scheme provides general-purpose, powerful, and orthogonal tools but it lacks many of the convenience features we tend to expect from a modern-day language. I spent a significant amount of time configuring the editor, setting up libraries, implementing some of Clojure's core utility functions, and experimenting with interactive development. On one hand, I learnt a lot. On the other, it was time away from working on the app.

I overall like the language. The selling points of Chez Scheme are its macros and that it is compiled. For this app, where binary size matters and users expect it to be fast from the get-go, I think Chez Scheme is a good match. If I were developing server-side software where startup time or binary size isn't a big deal, I would choose Clojure. Though GraalVM might also be worth looking into. If I were to choose Scheme, I would probably pick up Racket. It is based on Chez Scheme and provides many convenience features I wish Chez had.