macro_curious
macro_curious

Reputation: 43

How can this racket code for an anaphoric -> or ->> macro be improved?

I want to create a Racket macro which includes the functionality of one of Clojure's threading macros, but in addition requires an anaphoric argument (for example it), permitting me to specifically indicate where each previous value should be inserted into the next function call.

I’ll start with the ->> macro (ported to scheme or racket) where each new function call implicitly inserts the value of the previous function as its last argument:

(define-syntax ->>  
  (syntax-rules ()  
    [(_ x) x]  
    [(_ x (y ...) rest ...)  
     (->> (y ... x) rest ...)]))  

(->> (range 10)  
     (map add1)  
     (apply +)  
     (printf "the sum is: ~a\n"))  

; "the sum is: 55"  

I now define a macro named >> using syntax-case: Of course, I'll only need this new macro when it is not necessarily the last argument (nor always the first argument when using Clojure's -> macro).

(define-syntax (>> stx)  
  (syntax-case stx ()  
    [(_ x) #'x]  
    [(_ x (y ...) rest ...)  
     (with-syntax ([it (datum->syntax stx 'it)])  
       #'(let ([it x])  
           (>> (y ...) rest ...)))]))  

(>> (range 10)  
    (map add1 it)  
    (take it 5)  ; 'it' is not the last argument  
    (apply + it))  

Unfortunately, when evaluated the macro returns 45 rather than 15.

DrRacket's macro stepper shows the expansion to be:

(let ([it (range 10)])  
  (let ([it (map add1 it)])  
    (let ([it (take it 5)]) (apply + it))))  

The stepper also indicates that the final it is bound to the value returned by (range 10). Given this information, the return value of 45 is explained. But why is this the case? Can anyone help me to correct this lexical scoping problem?

By the way, the macro written slightly differently with syntax-parameterize works fine:

(require racket/stxparam)  
(define-syntax-parameter it  
  (lambda (stx)  
    (raise-syntax-error #f "can only be used inside '>>'" stx)))  

(define-syntax >>  
  (syntax-rules ()  
    [(_ x) x]  
    [(_ x (y ...) rest ...)  
      (let ([val x])  
        (syntax-parameterize ([it (make-rename-transformer #'val)])  
          (>> (y ...) rest ...)))]))  

Upvotes: 4

Views: 867

Answers (3)

user683995
user683995

Reputation: 1

I would say your first macro radically differs from the second version, first and foremost because it introduces sharing. Primarily, this allows the anaphoric binding to be used more than once (or maybe not at all). I would call the sharing version >>= to make this more explicit ;-)

Why? Because if the binding is not used, then all expressions above the anaphorically oblivious one should still be executed for effects (this should empathically not be the case for the pure rewriting version of >>, which I leave as an exercise)

In any case, you don't need the power of syntax-cases (assuming you want to stick with a macro solution). Here is a way to encode an anaphoric threading macro using Petrofsky extraction and plain syntax-rules:

(define-syntax >>= (syntax-rules ()
 ([_ th fa] fa)
 ([_ th fa . fb]
  (let-syntax ([K (syntax-rules () ([_ (this) ts]
     (let ([this fa])
       (>>= this . ts)))
   )])
   (extract* (th)
      fb (K [] fb))
   ))
 ))

Your test still works:

(>>= _
    (range 10)  
    (map add1 _)  
    (take _ 5)   
    (apply + _)) ;; => 15

P.S. Hint: to the get >> (the rewriting version of >>=), one only needs to change just one form in the definition of >>= ;-)

P.P.S. Here are syntax-rules for the extract*, courtesy Al Petrofsky & Oleg Kiselyov

(define-syntax-rule [extract symb body _k]
   (letrec-syntax ([tr (syntax-rules (symb)
     ([_ x symb tail (k symb-l . args)]
      (k (x . symb-l) . args))
     ([_ d (x . y) tail k]
      (tr x x (y . tail) k))
     ([_ d1 d2 () (k  symb-l . args)]
      (k (symb . symb-l) . args))
     ([_ d1 d2 (x . y) k]
      (tr x x y k)))])
      (tr body body () _k))
   )
(define-syntax extract* (syntax-rules ()
    ([_ (symb) body k]
     (extract symb body k))
    ([_ _symbs _body _k]
     (letrec-syntax ([ex-aux
       (syntax-rules ()
         ([_ found-symbs () body k]
          (reverse () found-symbs k))
         ([_ found-symbs (symb . symb-others) body k]
          (extract symb body
             (ex-aux found-symbs symb-others body k)))
         )]
      (reverse  
       (syntax-rules ()
         ([_ res () (k' () . args)]
          (k' res . args))
         ([_ res (x . tail) k]
          (reverse (x . res) tail k)))))
       (ex-aux () _symbs _body _k)))))

Upvotes: 0

Jack
Jack

Reputation: 2273

A more radical approach is to not make >> a macro at all:

(define (>> it . fs)
  ((apply compose
          (reverse fs))
   it))

(>> 5 add1 even?) ;; => #t

Now it's merely a function that combines the functions given to it into one function and calls that combined function with it's first argument it. The advantage here is you can decouple macros that produce anonymous functions in pretty ways from the logic of combining functions. For instance, using the fancy-app package which lets you make anonymous functions by using _ as a placeholder for an argument:

(>> (range 10)
    (map add1 _)
    (take _ 5)
    (apply + _))

This behaves as you expect, and >> no longer needs to be a macro so you can use it with functions that produce other functions, and it's just more flexible in general. You can get the fancy-app package with raco pkg install fancy-app and using it in code just needs (require fancy-app)

Upvotes: 4

soegaard
soegaard

Reputation: 31147

Here is one way to do it. First define a version of >> that uses an explicit it (below it is called >>>). The definition of >> simply generates an identifier and hands it to >>>.

#lang racket
(define-syntax (>>> stx)  
  (syntax-case stx ()  
    [(_ it x) #'x]  
    [(_ it x (y ...) rest ...)
     #'(let ([it x])  
         (>>> it (y ...) rest ...))]))

(>>> it
    (range 10)  
    (map add1 it) 
    (take it 5)  
    (apply + it))

(define-syntax (>> stx)
  (with-syntax ([it (datum->syntax stx 'it)])
    (syntax-case stx ()
      [(_ . more)
       #'(>>> it . more)])))

(>> (range 10)  
    (map add1 it) 
    (take it 5)  
    (apply + it))

The output is:

15
15

Note: I prefer your syntax-parameterize version.

Upvotes: 4

Related Questions