The SMoL Language Family
The SMoL languages accompany the third edition of PLAI.
There is a core set of shared semantic features in most widely-used languages, ranging from Java and Python to Racket and OCaml to Swift and JavaScript and beyond. Most contemporary mainstream programmers program atop a language built atop it. That makes it worth understanding.
SMoL, which stands for Standard Model of Languages, embodies this common core. As the name suggests, it also strips these languages to their essence. This aids understanding by eliminating features that are either not universal or are only incidental to understanding the core.
1 The smol/fun Language
#lang smol/fun | package: smol |
1.1 Definitions
TODO: include
1.2 Expressions
The base expression values are numbers, strings, symbols, Booleans. The language also permits, but does not provide useful operations to work with, list constants, vector constants, and more exotic quoted forms. (If you don’t know what these are, ignore them.)
procedure
elem : Any
procedure
expr : Vec
procedure
vec-expr : Vec idx-expr : Number
procedure
elem-l : Any elem-r : Any
procedure
expr : Pair
procedure
expr : Pair
procedure
expr : Any
1.3 Applications
Functions may not be passed as parameters.
1.4 Testing and Debugging
Testing and debugging are intertwined. The more tests you write, the less debugging work you will have to do. This is because tests localize debugging: if f calls g calls h and the result of a call to f isn’t what you expect, you have no idea where the problem might lie. But if you have good tests for some of these functions, then you have a fairly safe bet that the problem is in the ones for which you don’t. The more robustly you test, the farther you push the boundary of trust, and the less effort you have to later spend debugging.
The forms test, test/pred, and test/exn are all available from plai. Also provided is print-only-errors, which is useful to suppress good news. To these, SMoL adds
Therefore, at any point in the program, to study the value a particular expression takes, just wrap it in spy. It continues to produce a value, while the output shows both the source expression (which is helpful if you have multiple spys) as well as the source location (in case you inspect multiple locations that have the same source term).
1.5 Inherited from Racket
The constructs trace, untrace, provide, all-defined-out, let, let*, if, and, or, not, eq?, equal?, begin, +, -, *, /, zero?, <, <=, >, >=, and string=? are all inherited directly from Racket and behave exactly as they do there.
2 The smol/state Language
#lang smol/state | package: smol |
The smol/state language includes all of The smol/fun Language, and the following in addition.
The set! construct from Racket, which changes the values that variables are bound to.
The begin construct from Racket is also available, to sequence operations.
procedure
elem : Any
procedure
vec : Vec idx : Num val : Any
procedure
elem-l : Any elem-r : Any
procedure
expr : Pair val : Any
procedure
expr : Pair val : Any
2.1 Applications
Functions may not be passed as parameters.
3 The smol/hof Language
#lang smol/hof | package: smol |
the constructs letrec, lambda, and λ (which is just an alias for lambda),
the list generators, cons, empty, and list, and
the functions map, filter, foldl, and foldr.
3.1 Applications
Functions may be passed as parameters. This is the main point of this language.
4 The smol/dyn-scope-is-bad Language
#lang smol/dyn-scope-is-bad | package: smol |
Do not use this language!
In this language, we get to explore dynamic scope (at least one variant of it). The language’s name is intentionally chosen to pass value judgment on this feature.
This language currently provides dynamic scoping behavior for the binding forms defvar, deffun, lambda, λ, and let. For now let* and letrec aren’t present. The former is out of implementor laziness, but it’s a useful puzzle to ponder is why letrec hasn’t been provided.
Observe that hovering over variables in DrRacket does not present binding arrows. This is as it should be.
Finally, note that Compatible Use in Racket is going to produce very strange behavior in conjunction with Racket’s own binding forms like define. Have fun.
5 The smol/cc Language
#lang smol/cc | package: smol |
In this language we return to regular scope, building on the smol/hof language. Here we add call/cc and let/cc.
6 Compatible Use in Racket
If you want to program in some other language (typically racket) and would like to use constructs defined in SMoL, you can use the compat languages that are defined for each SMoL level by appending compat to the language name. For instance, smol/fun/compat is the compatibility layer for smol/fun.
#lang smol/fun (defvar x 3) (++ "x" (spy (++ "y" "z" (++))))
#lang racket (require smol/fun/compat) (defvar x 3) (++ "x" (spy (++ "y" "z" (++))))
The compatibility layers provide only the names provided by each of the languages; they do not provide any of the language restrictions. Thus, for instance, if you import smol/fun/compat, you can still use higher-order functions in Racket as you normally would.
Warning: The intent is that using this compatibility layer will leave the behavior of programs unchanged. However, if you import these bindings into a language with significantly different behavior than Racket, what they do is undefined. It’s safe to think of this as a Racket compatibility layer; it does not (nor can it) attempt to preserve the semantics in all other languages. For example, spy depends on being able to generate terminal output, but if the host language forbids any output, then spy may also be compromised, depending on how the host language has been implemented.