Macro Output Spliced into Context

I was just about to give up on doing the below, when I thought it might be fun for a more experienced person to offer advice. I have this repetitive cond for a comparator, with my thanks to @notjack for writing such important data structures for us all in rebellion.

(cond
  [(and      (null? left-scope)       (null? right-scope))  equivalent]
  [(and      (null? left-scope)  (not (null? right-scope))) lesser]
  [(and (not (null? left-scope))      (null? right-scope))  greater]
  [(<               left-scope               right-scope)   lesser]
  [(>               left-scope               right-scope)   greater]
  [(and      (null? left-tag)         (null? right-tag))    equivalent]
  [(and      (null? left-tag)    (not (null? right-tag)))   lesser]
  [(and (not (null? left-tag))        (null? right-tag))    greater]
  [(string<?        left-tag                 right-tag)     lesser]
  [(string>?        left-tag                 right-tag)     greater]
  [else                                                     equivalent])))

To reduce eye strain and improve readability, I thought I could express it this way instead:

(bulk-cond
  (null-pick   left-scope right-scope)
  (strict-pick left-scope right-scope < >)
  (null-pick   left-tag   right-tag)
  (strict-pick left-tag   right-tag string<? string>?)
  (else equivalent))

I understand this would be an academic exercise, but Racket came from academia; I should be among friends on wanting to do this. So, I went to work with a one-shot approach:

(define-syntax bulk-cond
  (syntax-rules (null-pick strict-pick)
    [(_ (null-pick left right) more ...)
     (bulk-cond
      [(and      (null? left)       (null? right))  equivalent]
      [(and      (null? left)  (not (null? right))) lesser]
      [(and (not (null? left))      (null? right))  greater]
      more ...)]
    [(_ (strict-pick left right on< on>) more ...)
     (bulk-cond
      [(on< left right) lesser]
      [(on> left right) greater]
      more ...)]
    [(_ more ...)
     (cond more ...)]))

But I had a flaw in my recursion that I could not bridge, so I through let me build the constituent parts:

(define-syntax-rule (null-pick left right)
  (list ;will need to splice this later
   [(and      (null? left)       (null? right))  equivalent]
   [(and      (null? left)  (not (null? right))) lesser]
   [(and (not (null? left))      (null? right))  greater]))

(define-syntax-rule (strict-pick left right on< on>)
  (list ;will need to splice this later
   [(on< left right) lesser]
   [(on> left right) greater]))

But I could not stitch the two patterns together correctly. I know the below "cure" is worse than the ailment and is also wrong, but maybe you can offer ideas on how to proceed differently?

;wrap the above 2 macros in begin-for-syntax, with:
;(require (for-syntax rebellion/base/comparator)), then:
(define-syntax (bulk-cond stx)
  (syntax-case stx ()
    [(_ pre-chunks ...)
     (with-syntax ([(post-chunks ...)
                    (for/list ([head (in-list (syntax->list #'(pre-chunks ...)))])
                      (if (memq (syntax-e head) '(null-pick strict-pick))
                        ???  ;some kind of spliced call into sub-cases
                        ???))]) ;leave the else case alone
       #'(cond post-chunks ...))]))

Or maybe something simpler, where I just express what I need correctly instead of doing it:

(define-syntax (bulk-cond stx)
  (syntax-case stx (null-pick strict-pick else)
    [(_ (null-pick n-rest ...) ... (strict-pick s-rest ...) ... o-rest ...)
     #`(cond #,@(null-pick   n-rest ...) ...
             #,@(strict-pick s-rest ...) ...
             o-rest ...)]))
;I know this is even more wrong, but I'm flailing at this point

Would you kindly attempt a suggestion in pure Racket? I barley have a handle on what I know that I don't think I can expand to syntax-parse or other libraries, unless you really think it would be more straightforward. Anyway, thanks for reading so far.

1 Like

I believe comparator-chain can help you here:

(define null<=>
  (comparator-map
    (comparator-of-constants 'null 'not-null)
    (lambda (v) (if (null? v) 'null 'not-null))))

(struct data (scope tag) #:transparent)

(define data<=>
  (comparator-chain
    (comparator-map null<=> data-scope)
    (comparator-map real<=> data-scope)
    (comparator-map null<=> data-tag)
    (comparator-map string<=> data-tag)))

(compare data<=> (data left-scope left-tag) (data right-scope right-tag))

Edit: fixed a bug where I forgot an if in the definition of null<=>.

Actually, that didn't quite work the way I hoped: (compare data<=> (data '() "apple") (data '() "banana")) throws an exception because it tries to compare '() and '() with real<=>. To fix that, I made this:

(define (nulls-first-comparator cmp)
  (make-comparator
   (λ (left right)
     (cond
       [(and (null? left) (null? right)) equivalent]
       [(null? left) lesser]
       [(null? right) greater]
       [else (compare cmp left right)]))))

(struct data (scope tag) #:transparent)

(define data<=>
  (comparator-chain
    (comparator-map (nulls-first-comparator real<=>) data-scope)
    (comparator-map (nulls-first-comparator string<=>) data-tag)))

(compare data<=> (data left-scope left-tag) (data right-scope right-tag))

Thank you @notjack for this and for being a great Racketeer that made me feel welcome in this community just by virtue of all the care you put into your libraries, documentation, and presentations. I'm a big fan of yours, especially your ambitious work on resyntax. Please don't mind me; I feel this way about most people that contributed to Racket as much as you did.

Your most recent change passed my unit tests for every case. Thanks for steering me in the right direction! I did not need macros at all, after spending a whole day trying to write the right one :slight_smile:.

Just because it might be fun for you to know what I'm using your code for, I'm about to use a sorted-map with a key that is a pair, and trying to supply a comparator for it. The pair is of an immutable scope reference (for a point in somebody's program), and a variable that is declared in that scope, or null. I'm trying to do something related to the genesis of resyntax as narrated in the last RacketCon presentation on the subject.

Our interests might be similar, except for my background being a business analyst who oversees the quality of work of other business analysts, and constantly thinking about how to do that systematically.

Here, I plan on filling the sorted-map with a pass of what was declared where so that it is only a matter of querying that data structure to find things like who called what, what is being called by whom, and the like. Not sure if knowing this interests you, but I'm sharing in case it does, because I'd enjoy discussing your side of it, if you like.

In any case, thanks again for your help and publishing very elegant code in our favorite language Racket.

That sounds interesting; I'd be happy to hear more. And I'm glad to know you've gotten some use out of the things I've made :slight_smile:

As for why the following code doesn’t work.

(define-syntax bulk-cond
  (syntax-rules (null-pick strict-pick)
    [(_ (null-pick left right) more ...)
     (bulk-cond
      [(and      (null? left)       (null? right))  equivalent]
      [(and      (null? left)  (not (null? right))) lesser]
      [(and (not (null? left))      (null? right))  greater]
      more ...)]
    [(_ (strict-pick left right on< on>) more ...)
     (bulk-cond
      [(on< left right) lesser]
      [(on> left right) greater]
      more ...)]
    [(_ more ...)
     (cond more ...)]))

The way you wrote bulk-cond is that it compiles only the first clause. Therefore, you need a way to “advance” the compilation to the rest of the clauses in the recursive expansion.

One possibility is to use cond directly on the compiled clauses.

(define-syntax bulk-cond
  (syntax-rules (null-pick strict-pick)
    [(_ (null-pick left right) more ...)
     (cond
       [(and      (null? left)       (null? right))  'equivalent]
       [(and      (null? left)  (not (null? right))) 'lesser]
       [(and (not (null? left))      (null? right))  'greater]
       [else
        (bulk-cond more ...)])]
    [(_ (strict-pick left right on< on>) more ...)
     (cond
       [(on< left right) 'lesser]
       [(on> left right) 'greater]
       [else
        (bulk-cond more ...)])]
    [(_ more ...)
     (cond more ...)]))

Another possibility is to advance it through the last clause.

(define-syntax bulk-cond
  (syntax-rules (null-pick strict-pick else)
    [(_ (null-pick left right) more ...)
     (bulk-cond
      [(and      (null? left)       (null? right))  'equivalent]
      [(and      (null? left)  (not (null? right))) 'lesser]
      [(and (not (null? left))      (null? right))  'greater]
      more ...)]
    [(_ (strict-pick left right on< on>) more ...)
     (bulk-cond
      [(on< left right) 'lesser]
      [(on> left right) 'greater]
      more ...)]
    [(_ (else expr ...))
     (let () expr ...)]
    [(_ more1 more ...)
     (cond
       more1
       [else (bulk-cond more ...)])]))

Hello @sorawee, I think you're famous :smile:. I now understand both approaches you provided. Thanks for showing me how to do this correctly. Isn't there a typical approach to compose groups of statements into a larger context that is typically used from your experience?

I'm very sure I don't have the right exposure to these idioms to think to use them, because I struggled when searching for examples, but am I correct in that assumption? Macros flow nicely and elegantly when they are transformed from template to template, not when constructed statement by statement, like I was trying to do, right?

There is a way to do “splicing”, but it doesn’t take macro outputs. Instead, it takes syntax templates.

You can’t manipulate syntax templates in syntax-rules and define-syntax-rule, because these forms don't allow you to do any computation. You are limited to producing a template right away.

syntax-case + with-syntax could work, but an easier approach would be to use the syntax/parse library, which allows you to directly “build the constituent parts” via define-syntax-class. Then you can use ~@ for splicing.

(begin-for-syntax
  (define-syntax-class clause
    (pattern ({~literal null-pick} left right)
      #:with expanded
      #'([(and      (null? left)       (null? right))  'equivalent]
         [(and      (null? left)  (not (null? right))) 'lesser]
         [(and (not (null? left))      (null? right))  'greater]))
    (pattern ({~literal strict-pick} left right on< on>)
      #:with expanded
      #'([(on< left right) 'lesser]
         [(on> left right) 'greater]))
    (pattern ({~literal else} expr ...)
      #:with expanded
      #'([else expr ...]))))

(define-syntax-parse-rule (bulk-cond c:clause ...)
  (cond {~@ . c.expanded} ...))

Wow, that is very elegant, it flows so naturally, easy to read and reason about. Maybe I shouldn't be allergic so much to syntax/parse. It just feels like a luxury to know how to use it. Thanks again, you're a treasure to us all. I'll be referring to this from to time. One day I hope to collect many of Racket's common idioms in some centralized place in a format easy to lookup and play with for casual programmers that enjoy this stuff like myself.

I'm marking this as the answer. First, we have a great and thoughtful Racket community, full of kind and supportive leaders that I'm grateful for. Second, sometimes the answer is not a macro, but composability in functions. Same abstraction work in breaking apart and bringing together, just in terms of compiled functionality. Finally, if you really want to do unusual stuff, try syntax/parse. Don't be afraid of it being considered advanced and having an academic paper backing it, like I did. Most of Racket has such papers.

1 Like