4
\$\begingroup\$

I'm doing the Racket track on exercism.io and solved the grains exercise:

Write a program that calculates the number of grains of wheat on a chessboard given that the number on each square doubles.

(test cases etc on exercism)

So I wrote a straightforward recursive solution:

#lang racket

(provide square total)

(define (square n)
  (cond
    [(n . = . 1)    1]
    [(n . > . 1)    (* 2 (square (- n 1)))]
    [else           (error "invalid number:" n)]))

(define (sum n)
  (cond
    [(n . = . 1)    1]
    [(n . > . 1)    (+ (square n) (sum (- n 1)))]
    [else           (error "invalid number:" n)]))

(define (total)
  (sum 64))

Then I went to clean it up by factoring out the induction:

#lang racket

(provide square total)

(define-syntax (induction stx)
  (syntax-case stx ()
    ((induction base rule)
     (with-syntax ((n (datum->syntax stx 'n)))
       #'(lambda (n)
           (cond
             ((n . = . 1)   base)
             ((n . > . 1)   rule)
             (else          (error "invalid number:" n))))))))

(define square  (induction 1 (* 2 (square (- n 1)))))
(define sum     (induction 1 (+ (square n) (sum (- n 1)))))

(define (total)
  (sum 64))

I got a couple of questions:

1) Is that the most straightforward way to introduce a new binding for the macro user?

Like, this is quite wordy. I guess I could wrap the "with-syntax" part into another macro, 'cause right now this feels very "built out of raw plumbing" to me.

It's also annoying that "define-syntax-rule" doesn't give me "stx", so I don't see a way to introduce new bindings. I'd like to write something like:

(define-syntax-rule (induction base rule)
  (syntax-let ((n))
    (lambda (n)
      (cond
        ((n . = . 1)    base)
        ((n . > . 1)    rule)
        (else           (error "invalid number:"))))))

Is there anything built-in for that?

2) For argument clarity, I'd like to write it more like this:

(define (square n)
  (induction 1 (* 2 (square (- n 1)))))

How'd I do that? Like, I'd... have to capture the "n" argument correctly (how?), and return, uh... the body of the lambda? I'm confused.

\$\endgroup\$
2
\$\begingroup\$

When writing macros, it is best to avoid using functions like datum->syntax to conjure up identifiers. Instead, your macro should take the identifiers to bind from the macro use site.

For your example, I would write it like this:

#lang racket

(provide square total)

;; note how `n` is passed in as an argument
(define-syntax-rule (induction n base rule)
  (lambda (n)
    (cond [(n . = . 1) base]
          [(n . > . 1) rule]
          [else        (error "invalid number:" n)])))

(define square (induction n 1 (* 2 (square (- n 1)))))
(define sum    (induction n 1 (+ (square n) (sum (- n 1)))))

(define (total)
  (sum 64))

Macros written in this style are more composable and cooperate with macro hygiene. Racket's macro system lets you break this hygiene in a controlled way, but that is usually a bad idea.

Also, for a simple program like this I would usually not bother writing a macro. In this case, you could even have abstracted this using only first-class functions instead of macros.

You may also wish to read Macros and Languages in Racket and Fear of Macros if you haven't already.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.