Discussion 13: Macros

This is an online worksheet that you can work on during discussions and tutorials. Your work is not graded and you do not need to submit anything.

Discussion 13 Vitamin

To encourage everyone to watch or attend discussion orientation, we have created small discussion vitamins. Each vitamin you complete will give you an extra credit point towards the course. Please answer all of the questions in this form after discussion orientation and before tutorial. If you have tutorial right after discussion, please complete within 3 hours after.

Macros

Previously, we've seen how we can create macro-like functions in Python using f-strings. Now let's talk about real macros, in Scheme. So far we've been able to define our own procedures in Scheme using the define special form. When we call these procedures, we have to follow the rules for evaluating call expressions, which involve evaluating all the operands.

In the scheme project, we saw that special form expressions do not follow the evaluation rules of call expressions. Instead, each special form has its own rules of evaluation, which may include not evaluating all the operands. Wouldn't it be cool if we could define our own special forms where we decide which operands are evaluated? Consider the following example where we attempt to write a function that evaluates a given expression twice:

scm> (define (twice f) (begin f f))
twice
scm> (twice (print 'woof))
woof

Since twice is a regular procedure, a call to twice will follow the same rules of evaluation as regular call expressions; first we evaluate the operator and then we evaluate the operands. That means that woof was printed when we evaluated the operand (print 'woof). Inside the body of twice, the name f is bound to the value undefined, so the expression (begin f f) does nothing at all!

We have a problem here: we need to prevent the given expression from evaluating until we're inside the body of the procedure. This is where the define-macro special form, which has identical syntax to the regular define form, comes in:

scm> (define-macro (twice f) (list 'begin f f))
twice

define-macro allows us to define what's known as a macro, which is simply a way for us to combine unevaluated input expressions together into another expression. When we call macros, the operands are not evaluated, but rather are treated as Scheme data. This means that any operands that are call expressions or special form expression are treated like lists.

If we call (twice (print 'woof)), f will actually be bound to the list (print 'woof) instead of the value undefined. Inside the body of define-macro, we can insert these expressions into a larger Scheme expression. In our case, we would want a begin expression that looks like the following:

(begin (print 'woof) (print 'woof))

As Scheme data, this expression is really just a list containing three elements: begin and (print 'woof) twice, which is exactly what (list 'begin f f) returns. Now, when we call twice, this list is evaluated as an expression and (print 'woof) is evaluated twice.

scm> (twice (print 'woof))
woof
woof

To recap, macros are called similarly to regular procedures, but the rules for evaluating them are different. We evaluated lambda procedures in the following way:

  1. Evaluate operator
  2. Evaluate operands
  3. Apply operator to operands, evaluating the body of the procedure

However, the rules for evaluating calls to macro procedures are:

  1. Evaluate operator
  2. Apply operator to unevaluated operands
  3. Evaluate the expression returned by the macro in the frame it was called in.

Questions

Q1: Or Macro

Implement or-macro, which takes in two expressions and or's them together (applying short-circuiting rules). However, do this without using the or special form. You may also assume the name v1 doesn't appear anywhere outside this macro.

The behavior of the or macro procedure is specified by the following doctests:

scm> (or-macro (print 'bork) (/ 1 0))
bork
scm> (or-macro (= 1 0) (+ 1 2))
3
Run in 61A Code

Q2: Replicate

Write a macro that takes an expression expr and a number n and repeats the expression n times. For example, (repeat-n expr 2) should behave the same as (twice expr) from the introduction section of this worksheet. It's possible to pass in a combination as the second argument (e.g. (+ 1 2)) as long as it evaluates to a number. Be sure that you evaluate this expression in your macro so that you don't treat it as a list.

Complete the implementation for repeat-n so that its behavior matches the doctests below.

scm> (repeat-n (print '(resistance is futile)) 3)
(resistance is futile)
(resistance is futile)
(resistance is futile)

scm> (repeat-n (print (+ 3 3)) (+ 1 1))  ; Pass a call expression in as n
6
6

You may find it useful to implement the replicate procedure, which takes in a value x and a number n and returns a list with x repeated n times. We've provided some examples for how replicate should function here:

scm> (replicate '(+ 1 2) 3)
((+ 1 2) (+ 1 2) (+ 1 2))

scm> (replicate (+ 1 2) 1)
(3)

scm> (replicate 'hi 0)
()

Hint: Feel free to check out Discussion 11 and copy over your implementation of replicate!

Hint 2: Consider which method to build your list (list, cons, or quasiquote) might help make your implementation simpler.

Run in 61A Code

Q3: List Comprehensions

Recall that list comprehensions in Python allow us to create lists out of iterables:

[<map-expression> for <name> in <iterable> if <conditional-expression>]

Define a procedure to implement list comprehensions in Scheme that can create lists out of lists. Specifically, we want a list-of macro that can be called as follows:

(list-of <map-expression> 'for <name> 'in <list> 'if <conditional-expression>)

The symbols for, in, and if must be quoted when calling list-of so that they will not be evaluated. The program will error if they have not been previously defined.

Calling list-of will return a new list constructed by doing the following for each element in <list>:

  • Bind <name> to the element.
  • If <conditional-expression> evaluates to a truth-y value, evaluate <map-expression> and add it to the result list.

Here are some examples:

scm> (list-of '(* x x) 'for 'x 'in ''(3 4 5) 'if '(odd? x))
(map (lambda (x) (* x x)) (filter (lambda (x) (odd? x)) (quote (3 4 5))))

scm> (eval (list-of '(* x x) 'for 'x 'in ''(3 4 5) 'if '(odd? x)))
(9 25)

scm> (list-of ''hi 'for 'x 'in ''(1 2 3 4 5 6) 'if '(= (modulo x 3) 0))
(map (lambda (x) (quote hi)) (filter (lambda (x) (= (modulo x 3) 0)) (quote (1 2 3 4 5 6))))

scm> (eval (list-of ''hi 'for 'x 'in ''(1 2 3 4 5 6) 'if '(= (modulo x 3) 0)))
(hi hi)

scm> (eval (list-of '(car e) 'for 'e 'in ''((10) 11 (12) 13 (14 15)) 'if '(list? e)))
(10 12 14)

Hint: You may use the built-in map and filter procedures. Check out the Scheme Built-ins reference for more information.

You may also find it helpful to look at the expression returned by list-of in the example above.

Run in 61A Code

Q4: List Comprehension Macro

We managed to create a version of list comprehension in scheme, but we had to add a few extra steps to our implementation. We needed to quote parameters, make an extra call to eval at the end to actually get our answer, and quote the symbols for, in, and if to prevent our program from throwing an Error.

To make things easier, we can use the define-macro special form to simplify this process.

Now use a macro to implement the list-of macro that can be called as follows:

(list-of <map-expression> for <name> in <list> if <conditional-expression>)

Calling list-of will return a new list constructed by doing the following for each element in <list>:

  • Bind <name> to the element.
  • If <conditional-expression> evaluates to a truth-y value, evaluate <map-expression> and add it to the result list.

Here are some examples:

scm> (list-of (* x x) for x in '(3 4 5) if (odd? x))
(9 25)

scm> (list-of 'hi for x in '(1 2 3 4 5 6) if (= (modulo x 3) 0))
(hi hi)

scm> (list-of (car e) for e in '((10) 11 (12) 13 (14 15)) if (list? e))
(10 12 14)

Hint: Again, you may use the built-in map and filter procedures. Check out the Scheme Built-ins reference for more information.

If you're having a hard time with the question revisit Question 7 (If Macro Scheme) from Discussion 12.

Run in 61A Code

Q5: Shapeshifting Macros (Tutorial)

When writing macros in Scheme, we often create a list of symbols that evaluates to a desired Scheme expression. In this question, we'll practice different methods of creating such Scheme lists.

We have executed the following code to define x and y in our current environment.

(define x '(+ 1 1))
(define y '(+ 2 3))

We want to use x and y to build a list that represents the following expression:

(begin (+ 1 1) (+ 2 3))

What would be the result of calling eval on a quoted version of the expression above?

(eval '(begin (+ 1 1) (+ 2 3)))

Now that we know what this expression should evaluate to, let's build our scheme list.

How would we construct the scheme list for the expression (begin (+ 1 1) (+ 2 3)) using quasiquotation?

How would we construct this scheme list using the list special form?

How would we construct this scheme list using the cons special form?

Q6: Max Macro (Tutorial)

Define the macro max, which takes in two expressions expr1 and expr2 and returns the maximum of their values. If they have the same value, return the first expression. For this question, it's okay if your solution evaluates expr1 and expr2 more than once. As an extra challenge, think about how you could use the let special form to ensure that expr1 and expr2 are evaluated only once.

scm> (max 5 10)
10
scm> (max 12 12)
12
scm> (max 100 99)
100

First, try using quasiquotation to implement this macro procedure.

Run in 61A Code

Now, try writing this macro using the list special form.

Run in 61A Code

Finally, write this macro using the cons special form.

Run in 61A Code

Q7: When Macro (Tutorial)

Using macros, let's make a new special form, when, that has the following structure:

(when <condition>
      (<expr1> <expr2> <expr3> ...))

If the condition is not false (a truthy expression), all the subsequent operands are evaluated in order and the value of the last expression is returned. Otherwise, the entire when expression evaluates to okay.

scm> (when (= 1 0) ((/ 1 0) 'error))
okay

scm> (when (= 1 1) ((print 6) (print 1) 'a))
6
1
a
Run in 61A Code