Racket macro: cond/define

I wrote a macro and wrote a blog post about it: Racket macro: cond/define - cadence's weblog (personal blog)


Here's the macro:

(define-syntax (cond/define stx)
  (syntax-parse stx
    ;; terminating with else
    [(_ [(~datum else) expr ...])
     #'(begin expr ...)]
    ;; continuing with define
    [(_ ((~datum define) name expr) cond ...)
     #'(let ([name expr])
         (cond/define cond ...))]
    ;; continuing with a condition
    [(_ [condition expr ...] cond ...)
     #'(if condition
           (begin expr ...)
           (cond/define cond ...))]))

You are welcome to use, reproduce and modify this macro without restriction. Attribution is optional.

My blog post also talks about how the macro works. This is not the most well-explained macro tutorial on the web, but it is a tutorial I wish I had seen a few months ago when I was just barely dipping my toes into syntax-parse. In particular, my post shows how to use ~datum, ..., recursive macros, and describes the inscrutable error if you forget to (require (for-syntax racket/base syntax/parse)), which are all things I really struggled with initially.

In the future I might write another similar post where I show how and why to use #:with, macros in Typed Racket, and #,@#'.

4 Likes

A few ideas that occur to me:

  • given the prevalence of macros like this, should Rhombus's multi-arm conditional forms allow intermediate defines?
  • You might want to use (let () expr ...) for your clauses to permit internal definitions (try (cond/define [else (define x 1) (add1 x)]) and compare with Racket's cond).
  • The pattern matching on define and else could be ~literal, since Racket has bindings for both.
  • You might want to allow empty (cond/define) as your base case so that (cond/define [1 2]) and similar from Racket work.
1 Like

It is a good idea. I had written such a macro that is integrated in Scheme+ (cf. 1), the macro is 'condx' named because it allows eXecution of code between each 'cond' clause.

Here is the code of my macro:

(define-syntax condx
  (syntax-rules (exec else)
    ((_) '()) ;; allow no else clause
    ((_ (else e ...))
     (let () e ...))
    ((_ (exec s ...) d1 ...)
     (let () s ... (condx d1 ...)))
    ((_ (t e ...) tail ...)
     (if t
         (let () e ...)
         (condx tail ...)))))

note that it did not use syntax features because i did not knew too much about that at the time i wrote it, also i wanted to be compatible with Scheme, notonly Racket.

and a running example is:

(define x 1)
(condx ((= x 7) 'never)

       (exec
          (define y 3)
          (set! x 7))

       ((= y 1) 'definitely_not)

       (exec
          (set! y 10)
          (define z 2))

       ((= x 7) (+ x y z))
        
       (else 'you_should_not_be_here))

 19 

condx is a bit different of cond/define but they both have the same goal: make code easier to read, avoid nested cond or let .

Another solution to avoid nested 'cond' and 'if' is to have some 'return' features from a Scheme procedure.

This is for the reason i developped 'return' and return recursively 'return-rec' and the 'def' macro for defining procedures using 'return' or 'return-rec'.

Then you can do all the coding only with 'if' and 'return' without using 'cond', explanation can be found on this page (2) and examples too.

References:

1 : Scheme-PLUS-for-Racket/src/condx.scm at main · damien-mattei/Scheme-PLUS-for-Racket · GitHub

2: [“Scheme+ programing language by Damien Mattei”] | [“Enhancing Scheme (and Lisp-like) languages.”]

[“Scheme+ programing language by Damien Mattei”] | [“Enhancing Scheme (and Lisp-like) languages.”]

1 Like

The package parendown is another (and more general) way of eliminating the tail-recursive
nesting that otherwise would indent your code off the page.

https://docs.racket-lang.org/parendown/index.html

-- hendrik

2 Likes

Yes, early return would definitely avoid needing workarounds like these.

1 Like

[G]iven the prevalence of macros like this, should Rhombus's multi-arm conditional forms allow intermediate defines?

I’m not sure what that should look like, since we don’t allow arbitrary groups among alts. Putting the intermediate groups inside an alt makes the (apparent) scoping quite confusing. (And intermediate definitions among branches really aren’t usual at all.)

And mechanisms like parendown can eliminate the deep nesting that might occur.
I like it because it's a much more general mechanism than these macros.

-- hendrik