When we invoke #lang jsonic, Racket will look for a "main.rkt" module within our jsonic directory. Within that module, it will expect to find a reader submodule that provides our read-syntax function. We’ll use require and provide to fetch this function from a "reader.rkt" module that we’ll create next. For now, let’s put a "main.rkt" in the jsonic directory that looks like this:
1 2 3 4
#lang br/quicklang (module reader br (require "reader.rkt") (provide read-syntax))
One more time—let’s recap the role of the reader:
The reader converts the source code of our language from a string of characters into Racket-style parenthesized forms, also known as S-expressions.
By convention, Racket expects the name of the main reader function to be read-syntax. This read-syntax function must return one value: code for a module expression, packaged as a syntax object. This syntax object must have no bindings.
In bf, we saw that we could make a read-syntax function by relying on a tokenizer and a parser to do the heavy lifting.
We’ll do the same here. In fact, we’ll just take the read-syntax from bf and reuse it here, changing the expander name from bf/expander to jsonic/expander. Our new "reader.rkt" ends up like this:
1 2 3 4 5 6 7 8 9
#lang br/quicklang (require "tokenizer.rkt" "parser.rkt") (define (read-syntax path port) (define parse-tree (parse path (make-tokenizer port))) (define module-datum `(module jsonic-module jsonic/expander ,parse-tree)) (datum->syntax #f module-datum)) (provide read-syntax)
By the way, we’re using parse-tree and module-datum as intermediate values for readability. We could also write this function more compactly like so:
1 2 3 4 5 6 7
#lang br/quicklang (require "tokenizer.rkt" "parser.rkt") (define (read-syntax path port) (datum->syntax #f `(module jsonic-mod jsonic/expander ,(parse path (make-tokenizer port))))) (provide read-syntax)
Because Racket is an expression-based language, it’s usually possible—and therefore tempting—to just keep nesting lines of code inside each other. But it’s not a virtuous habit.
First, it makes the code harder to read—for others, but especially for the future version of ourselves. What seems like cleverly compact notation today will just be an annoying brain-teaser next week.
Second, it doesn’t make the code faster. Before Racket runs a program, it optimizes the code. Most of these simple notational “improvements” will be done automatically by Racket anyhow. So we’re usually better off maximizing the readability of our code.
Our read-syntax function won’t work yet, because it relies on "tokenizer.rkt" and "parser.rkt". We’ll add those next.