The SMo  L Language Family
1 The smol/  fun Language
1.1 Definitions
defvar
deffun
1.2 Expressions
ivec
vec-len
vec-ref
pair
left
right
pair?
+  +
1.3 Applications
1.4 Testing and Debugging
test/  not
spy
1.5 Inherited from Racket
2 The smol/  state Language
mvec
vec-set!
mpair
set-left!
set-right!
2.1 Applications
3 The smol/  hof Language
3.1 Applications
4 The smol/  dyn-scope-is-bad Language
5 The smol/  cc Language
6 Compatible Use in Racket
8.10

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

    2 The smol/state Language

    3 The smol/hof Language

    4 The smol/dyn-scope-is-bad Language

    5 The smol/cc Language

    6 Compatible Use in Racket

1 The smol/fun Language

 #lang smol/fun package: smol

1.1 Definitions

syntax

(defvar id expr)

Defines a new identifier, id, and binds it to the value of expr.

syntax

(deffun (fun-id arg-id ...) def/expr ... expr)

The fun-id is the name of the function; the remaining arg-ids are its parameters. These are bound before evaluating def/exprs and expr. deffun permits recursive 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

(ivec elem ...)  Vec

  elem : Any

procedure

(vec-len expr)  Number

  expr : Vec

procedure

(vec-ref vec-expr idx-expr)  Any

  vec-expr : Vec
  idx-expr : Number
The ivec operation builds an immutable vector of the elements in elem. Vector elements need not be of the same type. vec-len computes its length, while vec-ref indexes into it (starting from 0).

procedure

(pair elem-l elem-r)  Vec

  elem-l : Any
  elem-r : Any

procedure

(left expr)  Any

  expr : Pair

procedure

(right expr)  Any

  expr : Pair

procedure

(pair? expr)  Boolean

  expr : Any
pair is a special-case of ivec: it creates a two-element vector. left and right access the left (index 0) and right (index 1) elements. pair? recognizes any two-element vector, not only just those built using pair.

procedure

(++ s ...)  String

  s : String
++ concatenates any number of strings.

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

syntax

(test/not result-expr not-expected-expr)

This is just like test, except the sense of equality is inverted. Sometimes it’s useful to write negative tests: tests that say a particular behavior will not happen. For instance, if you’re testing scopes and have two variables with the same name but different values, it’s expressive to say that a particular value (bound to the variable not in scope) will not show up.

syntax

(spy expr)

The spy construct is essentially “printf done right”, especially for expression-oriented languages. It prints both the source expression, source location, and resulting value of the enclosed expression. It then returns that value. Note that any expression can be wrapped, not only a variable.

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

(mvec elem ...)  Vec

  elem : Any

procedure

(vec-set! vec idx val)  Void

  vec : Vec
  idx : Num
  val : Any
mvec creates mutable vectors, and vec-set! modifies them. vec-set! cannot modify an immutable vector.

procedure

(mpair elem-l elem-r)  Vec

  elem-l : Any
  elem-r : Any

procedure

(set-left! expr val)  Void

  expr : Pair
  val : Any

procedure

(set-right! expr val)  Void

  expr : Pair
  val : Any
mpair creates mutable pairs (which are just two-element mutable vectors), and set-left! and set-right! modify them. The elements are accessed using left and right, as before.

2.1 Applications

Functions may not be passed as parameters.

3 The smol/hof Language

 #lang smol/hof package: smol

The smol/hof language includes all of The smol/state Language (with the exception of Applications, and the following in addition from Racket:
  • 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.

As an example, these two programs behave exactly the same way:
#lang smol/fun
 
(defvar x 3)
(++ "x" (spy (++ "y" "z" (++))))
and
#lang racket
 
(require smol/fun/compat)
 
(defvar x 3)
(++ "x" (spy (++ "y" "z" (++))))
but the latter gives you access to all of the rest of Racket as well. You could use compatibility layer because you find some of these constructs more familiar, comfortable, or convenient than their counterparts in Racket, but otherwise want to use Racket’s more powerful mechanisms (such as its macro system).

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.