More Examples of Creating New Syntax

Introduction

This reading is a set of four more examples of macros and creating new syntax:

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...

This enables me to pass in code with underscore-style functions and produces executable Racket code, as a Racket list.

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.