Every language made with Racket has two essential components:
A reader, which converts the source code of our language from a string of characters into Racket-style parenthesized forms, also known as S-expressions.
An expander, which determines how these parenthesized forms correspond to real Racket expressions (which are then evaluated to produce a result).
For every language, we’ll specify a reader and an expander. Sometimes we’ll write them from scratch. Sometimes we’ll import them from elsewhere. But they’re always mandatory.
They don’t need to be large, either. For stacker, we’ll write both the reader and expander. But they’ll both fit into 40 lines of code.
Every Racket source file begins with a #lang line. If you’ve explored Racket you might have seen variants like these:
1 2 3 4 | #lang racket #lang racket/base #lang scribble/doc #lang datalog |
In this book, we’ll primarily be using:
1 2 3 | #lang br #lang br/quicklang #lang brag |
As we just learned, the first step in interpreting a language is to send the source code to the reader for that language. The #lang line’s job is to tell Racket where to find that reader. The reader, in turn, will tell Racket where to find the expander.
The reader and expander for a language are often stored in different source files—or modules, as they’re also known in Racket. But that’s optional. For this project, we’ll implement the language with just one source file, called "stacker.rkt".
In the future, we’ll prefer to use the shorthand #lang name notation shown in the above examples. But that requires some extra setup. (We’ll learn how to do that in the next tutorial.)
For now, we’ll use the alternate #lang reader path notation, where path is a local path to the source file that contains our reader:
1 | #lang reader "stacker.rkt" |
We need to write some Racket code. So let’s learn some Racket:
The basic building block of Racket is the S-expression. Examples of S-expressions: a single value like 2 or "blue", a variable like edge, a list of values like (list 2 "blue" edge), or a function call like (* 21 2).
Function calls go between parentheses. All parenthesized S-expressions are treated as function calls. The function name sits in the first position inside the parentheses, followed by its arguments. Thus we write (* edge 4), not (edge * 4). This style is known as prefix notation. Parenthesized expressions like (4) or ("blue") will produce errors, because 4 and "blue" aren’t functions. (For more, see functions.)
Every S-expression is evaluated to produce a value. A variable evaluates to whatever value it holds. So after we say (define edge 2), edge would evaluate to 2. A function call evaluates to its return value, so (+ 2 2) would evaluate to 4. (For more, see evaluation.)
S-expressions can be nested to any depth. Thus, these expressions mean the same thing:
True and false are spelled #t and #f. In conditional forms like if, the only value that evaluates to false is #f. Everything else counts as true (even 0 or null). (For more, see Booleans and conditionals.)
Linebreaks and indentation are used for readability. They don’t change the meaning of the code.
Square brackets are sometimes used for readability. They mean the same thing as parentheses.
Beyond that, Racket has the usual numbers, strings, and functions that we know from other languages. We’ll handle those as we encounter them.
Open DrRacket. Start a new source file called "stacker.rkt" and save it in a convenient location. We’ll make this file with a special tutorial language called br/quicklang. The br/quicklang language comes from the beautiful-racket package we installed during the main setup. It works the same way as ordinary Racket, but adds some extra conveniences. For testing purposes, let’s add a second line, so our whole file looks like this:
1 2 | #lang br/quicklang 42 |
Click Run in the DrRacket toolbar—or learn the key shortcut, because we’ll be using it a lot. In the DrRacket interactions window, we’ll see the result:
1 | 42 |
In the same directory as "stacker.rkt", create a second source file called "stacker-test.rkt". We’ll use this to test our new language as we go (but it won’t work yet):
1 | #lang reader "stacker.rkt" |
If we run this file anyway, we get an error that looks like this:
1 | read-syntax: cannot find reader for `#lang reader "stacker.rkt"` |
If so, that’s the right result: we’re telling #lang to get a reader out of "stacker.rkt", but we haven’t made one yet.
So let’s do that.