More Examples of Creating New Syntax
Introduction
This reading is a set of four more examples of macros and creating new syntax:
- a template for simple functions that we process with a Racket function
-
an
assert
macro usingdefine-syntax-rule
- a look at C macros
-
redefining
apply
, which changes Racket at its lowest level
The zip file for this reading contains the code for all four examples.
Read any or all of them that interest you. Being able to change
Racket's base apply
function in order to change the
evaluator is pretty cool, though.
A Template for Simple Function
Back in Session 6, we had to
curry the string-ref
function
so that we could map
it across a list of strings:
(define first-char (lambda (str) (string-ref str 0))) (map first-char list-of-strings)
Of course, we could get by with less code if we didn't bother to name our helper function:
(map (lambda (str) (string-ref str 0)) list-of-strings)
I like Racket as much as anyone, but sometimes I wish I could write even simpler code, especially simple functions. Wouldn't it be nice if I could write something like this:
(map (string-ref _ 0) list-of-strings)
and have Racket create the anonymous function I need? I don't even care what it calls the parameter, as long as it doesn't clash with an existing name. (We have been writing pure functions all semester with few, if any global variables, so this isn't much of a worry.)
An underscore form of this sort is becoming common in other languages, especially for use in pattern matching. It would be really handy for simple arithmetic expressions, such as:
(map (* _ 5) (range 1 100)) (filter (< _ 10) (map f big-list))
This an example of a possible syntactic abstraction. We can write code to translate the abstraction into a core form. We need a preprocessor.
I wrote a little code to translate expressions of this form:
(... _ ...)
into expressions of this form:
(lambda (newvar) (... newvar ...))
It required a bit of work...
- generating a unique new symbol using
gensym
-
writing a
split
function that takes an underscore exp as input and pulls out its 'beginning' and its 'end' -
writing a
translate_
function that usessplit
to turn a simple underscore exp into an anonymouslambda
function -
writing a recursive preprocessor,
pp
, to handle nested expressions
This enables me to pass in code with underscore-style functions and produces executable Racket code, as a Racket list.
- run
pp
on a simple expression - run the result expression as code
- copy the generated code into a definitions file
- read exps from a file... and write them back
We can do this! We have the technology -- and you have the knowledge to write the preprocessor. Racket's simple, parenthesized syntax helps us here.
If only we could build this process into the language somehow: remove the friction, and let Racket do most of the work.
We can, with syntax-rules
.
An assert
Macro
Think of a simple "assert" operator that allows us to test an expression and stop computation if the test fails:
(assert (>= (length lst) 2))
This expression is an abstraction of an if
expression,
or a Racket unless
expression:
(unless (>= (length lst) 2) (error 'assert "assertion failed: ~s" (quote (>= (length lst) 2))))
Racket provides an operator called define-syntax-rule
for writing such translations. In
this file,
I define assert
as a new special form:
(define-syntax-rule (assert expr) (unless expr (error 'assert "assertion failed: ~s" (quote expr))))
define-syntax-rule
allows us to define patterns of
the form:
pattern → expansion
and effectively add them to Racket's preprocessor.
It is simpler than syntax-rules
, for cases where we
have a simple pattern, a simple template, and only one case to
handle.
C Macros
Note: I need to write more. If you want to read it now, let me know, and I will!
C has rudimentary macro systems. The C preprocessor works by simple textual search-and-replace at the token level, rather than at the character level. This allows some powerful forms of conditional processing, but working at the token level creates problems. If you are interested, check out this directory of examples.
Changing Racket at the Lowest Level:
Redefining apply
We have seen that we can instruct Racket's preprocessor to recognize new special forms at expansion time.
But Racket is more. We can modify how Racket applies functions!
This file
wraps Racket's apply
function in a function that also
prints trace information — and then exports it as Racket's
apply
function.
Wow. You definitely can't do that in Python, Java, or C++.
Too often, when we teach languages like Racket, we only show you things that other languages can do, sometimes more simply.
This is a common complaint. Here's an example I found on the internet somewhere:
*Wow*. Yeah, you definitely can't do that in C++. (You _can_ in Lisp but they don't teach you those parts at school. They teach the pure functional parts, where you can't do things that you can in C++. Bastards.)
I have lost track of the source of this quote. If you know, please let me know. But I have never forgotten the story.
Examples like this one, and
range-case
and
Pollen
from Session 21, show how Racket really is different, even from its
ancestors. Racket enables us to modify its reader, its expander,
and its evaluator, including function application.
When you put all that together, Racket really is more.