Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scheme macro triggered by keyword which is not the head of a list

Suppose I want to trigger a Scheme macro on something other than the first item in an s-expression. For example, suppose that I wanted to replace define with an infix-style :=, so that:

(a := 5) -> (define a 5)
((square x) := (* x x)) -> (define (square x) (* x x))

The actual transformation seems to be quite straightforward. The trick will be getting Scheme to find the := expressions and macro-expand them. I've thought about surrounding large sections of code that use the infix syntax with a standard macro, maybe: (with-infix-define expr1 expr2 ...), and having the standard macro walk through the expressions in its body and perform any necessary transformations. I know that if I take this approach, I'll have to be careful to avoid transforming lists that are actually supposed to be data, such as quoted lists, and certain sections of quasiquoted lists. An example of what I envision:

(with-infix-define
   ((make-adder n) := (lambda (m) (+ n m)))

   ((foo) :=
       (add-3 := (make-adder 3))
       (add-6 := (make-adder 6))
       (let ((a 5) (b 6))
           (+ (add-3 a) (add-6 b))))

   (display (foo))
   (display '(This := should not be transformed))

So, my question is two-fold:

  1. If I take the with-infix-define route, do I have to watch out for any stumbling blocks other than quote and quasiquote?
  2. I feel a bit like I'm reinventing the wheel. This type of code walk seems like exactly what standard macro expanding systems would have to do - the only difference is that they only look at the first item in a list when deciding whether or not to do any code transformation. Is there any way I can just piggyback on existing systems?
like image 612
Ord Avatar asked May 28 '12 23:05

Ord


2 Answers

  1. Before you continue with this, it's best to think things over thoroughly -- IME you'd often find that what you really want a reader-level handling of := as an infix syntax. That will of course mean that it's also infix in quotations etc, so it would seem bad for now, but again, my experience is that you end up realizing that it's better to do things consistently.

  2. For completeness, I'll mention that in Racket there's a read-syntax hack for infix-like expressions: (x . define . 1) is read as (define x 1). (And as above, it works everywhere.)

  3. Otherwise, your idea of a wrapping macro is pretty much the only thing you can do. This doesn't make it completely hopeless though, you might have a hook into your implementation's expander that can allow you to do such things -- for example, Racket has a special macro called #%module-begin that wraps a complete module body and #%top-interaction that wraps toplevel expressions on the REPL. (Both of these are added implicitly in the two contexts.) Here's an example (I'm using Racket's define-syntax-rule for simplicity):

    #lang racket/base
    
    (provide (except-out (all-from-out racket/base)
                         #%module-begin #%top-interaction)
             (rename-out [my-module-begin #%module-begin]
                         [my-top-interaction #%top-interaction]))
    
    (define-syntax infix-def
      (syntax-rules (:= begin)
        [(_ (begin E ...)) (begin (infix-def E) ...)]
        [(_ (x := E ...))  (define x (infix-def E) ...)]
        [(_ E)             E]))
    
    (define-syntax-rule (my-module-begin E ...)
      (#%module-begin (infix-def E) ...))
    (define-syntax-rule (my-top-interaction . E)
      (#%top-interaction . (infix-def E)))
    

    If I put this in a file called my-lang.rkt, I can now use it as follows:

    #lang s-exp "my-lang.rkt"
    (x := 10)
    ((fib n) :=
     (done? := (<= n 1))
     (if done? n (+ (fib (- n 1)) (fib (- n 2)))))
    (fib x)
    
  4. Yes, you need to deal with a bunch of things. Two examples in the above are handling begin expressions and handling function bodies. This is obviously a very partial list -- you'll also want bodies of lambda, let, etc. But this is still better than some blind massaging, since that's just not practical as you can't really tell in advance how some random piece of code will end up. As an easy example, consider this simple macro:

    (define-syntax-rule (track E)
      (begin (eprintf "Evaluating: ~s\n" 'E)
             E))
    (x := 1)
    
  5. The upshot of this is that for a proper solution, you need some way to pre-expand the code, so that you can then scan it and deal with the few known core forms in your implmenetation.

  6. Yes, all of this is repeating work that macro expanders do, but since you're changing how expansion works, there's no way around this. (To see why it's a fundamental change, consider something like (if := 1) -- is this a conditional expression or a definition? How do you decide which one takes precedence?) For this reason, for languages with such "cute syntax", a more popular approach is to read and parse the code into plain S-expressions, and then let the actual language implementation use plain functions and macros.

like image 173
Eli Barzilay Avatar answered Nov 11 '22 00:11

Eli Barzilay


Redefining define is a little complicated. See @Eli's excellent explanation.

If on the other hand, you are content with := to use set! things are a little simpler.

Here is a small example:

#lang racket

(module assignment racket
  (provide (rename-out [app #%app]))

  (define-syntax (app stx)
    (syntax-case stx (:=)
      [(_ id := expr)
       (identifier? #'id)
       (syntax/loc stx (set! id expr))]      
      [(_ . more) 
       (syntax/loc stx (#%app . more))])))

(require 'assignment)

(define x 41)
(x := (+ x 1))
(displayln x)

To keep the example to a single file, I used submodules (available in the prerelease version of Racket).

like image 42
soegaard Avatar answered Nov 11 '22 00:11

soegaard