Arc Forumnew | comments | leaders | submitlogin
A macro for creating infix DSLs
15 points by cchooper 6149 days ago | 10 comments
The infix macro defined below allows you to turn any symbol into an infix operator and create your own DSL with infix syntax. For example:

  ; a DSL for infix arithmetic
  (arith
    (4 + 2 * (3 + 5) - 2 ^ 3))
  => 12

  ; a DSL for infix logic
  (logic
    (4 < 5 or 4 > 5 and 3 is 3))
  => t

  ;predicate calculus with infix logical constants
  (predicate-calculus
    (t -> nil -> t ^ nil <-> nil))
  => t
These were all created using the infix macro. infix takes a list of operators in the form (symbol precedence associativity) plus the expressions you want to evaluate:

  (infix ((+ 1 left) (* 2 right))
    (4 + 2 * 7))
  => 18
You can mix infix and prefix expressions together. The macro determines whether an expression is infix by checking if it has the supplied infix operators in the right locations. So you must be careful with something like (apply + '(1 2 3)) if + is infix.

  (infix ((+ 1 left)) (apply + '(1 2 3)))
  => Error!
To switch infix off, use prefix:

  (infix ((+ 1 left)) (prefix apply + '(1 2 3)))
  => 6
Below is the code, plus the definitions of arith, logic and predicate-calculus used above.

Bug reports are welcome, as are suggestions for making the code shorter without it becoming too unreadable. This is the first thing I've done in Arc so I probably missed a few tricks.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;

  (mac infix (ops . rest)
     `(do ,@(map [do-infix _ ops] rest)))

  (def do-infix (expn ops)
    (if (acons expn)
        (if (is 'prefix (car expn)) (map [do-infix _ ops] (cdr expn))
            (test-for-infix expn ops) (to-prefix (map [do-infix _ ops] expn) ops)
            (map [do-infix _ ops] expn))
        expn))

  (def test-for-infix (expn ops)
    (and (<= 3 (len expn))
         (odd (len expn))
         (is (len (drop-arguments (cdr expn))) (len (ops-only expn ops)))))

  (def right-assoc (xs)
    (if (is 3 (len xs)) (list xs.1 xs.0 xs.2)
        (list xs.1 xs.0 (right-assoc (cddr xs)))))
  
  (def left-assoc (xs)
    (if (is 3 (len xs)) (list xs.1 xs.2 xs.0)
        (list xs.1 (left-assoc (cddr xs)) xs.0)))
  
  (def precedence (sym ops)
    ((find [is sym _.0] ops) 1))
  
  (def associate (expn op ops)
    (if (is 'left ((find [is op _.0] ops) 2))
        (list (left-assoc:rev expn))
        (list (right-assoc expn))))
  
  (def to-prefix (expn ops)
    (let expn-ops (ops-only expn ops)
         (if expn-ops
             (withs 
              (highest-op (best (compare > [precedence _ ops]) expn-ops)
               start-end (find-start-and-end-ops highest-op expn-ops)
               start (* 2 (car start-end))
               end (+ 3 (* 2 (cdr start-end))))
              (to-prefix
               (join
                (subseq expn 0 start)
                (associate (subseq expn start end) highest-op ops)
                (subseq expn end))
               ops))
             (car expn))))

  (def find-start-and-end-ops (op ops)
    (let start (pos op ops)
         (cons start (+ start (find-end-op op (nthcdr start ops) -1)))))

  (def find-end-op (op ops end)
    (if (is op (car ops))
        (find-end-op op (cdr ops) (+ 1 end))
        end))

  (def drop-arguments (expn)
    (if expn
        (cons (car expn) (drop-arguments (cddr expn)))))

  (def ops-only (expn ops)
    (keep [pos _ (map car ops)] (drop-arguments (cdr expn))))
  
  (def subseq (seq start (o end (len seq)))
    (if (isa seq 'string)
        (let s2 (newstring (- end start))
             (for i 0 (- end start 1)
                  (= (s2 i) (seq (+ start i))))
             s2)
        (firstn (- end start) (nthcdr start seq))))
  
  ;;;; Implementation of arith ;;;;

  (mac arith body
     `(let ^ (rfn ^ (base power)
              (if (is 0 power) 1
                  (< power 0) (/ 1 (^ base (* -1 power)))
                  (* base (^ base (- power 1)))))
       (infix ((+ 1 left) (- 1 left) (* 2 left) (/ 2 left) (^ 3 left)) ,@body)))

  ;;;; Implementation of logic ;;;;

  (mac logic body
    `(infix ((and 1 left) (or 1 left) (is 2 left) (< 2 left) (> 2 right)
             (<= 2 left) (>= 2 left)) ,@body))

  ;;;; Implementation of predicate-calculus ;;;;

  (mac predicate-calculus body
     `(with 
       (-> (fn (ant conseq) (or (no ant) (and ant conseq)))
        ^ (fn (x y) (and x y))
        u (fn (x y) (or x y))
        <-> (fn (x y) (or (and x y) (no (or x y)))))
       (infix ((-> 2 right) (^ 1 left) (u 1 left) (<-> 3 left)) ,@ body)))


3 points by eds 6149 days ago | link

I already wrote a basic infix syntax for arc, which is currently on the arc wiki in infix.arc.

http://arclanguage.org/item?id=2610

My solution calls the first parameter as a function when the object in the functional position is a number or symbol. The math operators are then overridden in infix.arc to do infix evaluation when they get a function in their parameter list.

This eliminates the need for explicit infix and prefix macros (the sqrt call below uses prefix notation in the middle of the infix expression):

  (3 + 4 * 5 - (sqrt 36))
If we had first class macros we might even be able to convert the expression at compile time rather than run time.

I definitely think there is a lot to improve on, and your version of the infix macro has some features mine doesn't (e.g. associativity).

I don't know if my version would work for comparison logic... The program actually switches when the functional position is a number or symbol, and since t and nil are symbols, it would theoretically work.... But point is, my current version is not very extensible.

Feel free to improve on my code if you like.

-----

6 points by cchooper 6149 days ago | link

I'd seen your post so I was hesitant to post mine, but I think it's worth it because we're using two different approaches.

I'm trying to let the user specify what is/isn't infix, which is why I detect operators rather than numbers. I want the macro to have uses beyond infix arithmetic e.g. parsers expressed in BNF.

Also, mine is written entirely in Arc, so it's a good demonstration of what Arc can do, not to mention good practice for me.

So I think both our solutions have their place :)

-----

5 points by eds 6149 days ago | link

I fully support what you are doing, people might not want implicit infix math all the time. It also looks like you are thinking more toward how to let the user define infix operators, which my version doesn't do a good job with.

I actually rewrote my version in arc, except for 2 lines in ac.scm (one which implemented something pg was already thinking about, and one which allowed (eval (list + 3 4))). I posted a comment about it on my original post, but I think it got buried so far down that no one ever saw it.

-----

6 points by cadaver 6148 days ago | link

Please note that there is a space between ,@ and body in 'predicate-calculus, which is probably a typo.

-----

3 points by cchooper 6148 days ago | link

Thanks. Luckily, that doesn't seem to break it.

-----

3 points by nlavine 6148 days ago | link

This macro is excellent! Thank you!

I especially like the way it wraps the expressions you want in infix and doesn't affect ones outside of its body, allowing you to use infix syntax in one place and not make the rest of the program behave in unexpected ways.

This will be especially good one we can bring readtable support (or the equivalent) into arc. I saw this suggestion somewhere:

  <3 + 4 * 5> ; syntax for an infix math form.
And as you say, BNF parsers and whatever the notation for scanners is will be great.

-----

1 point by cchooper 6148 days ago | link

Thanks!

-----

1 point by absz 6148 days ago | link

Oooh, I like this! That's a very nice way to handle infix. Minor nitpick, though: why not just have arith be

  (mac arith body
    `(let ^ expt
       (infix ((+ 1 left) (- 1 left) (* 2 left) (/ 2 left) (^ 3 left)) ,@body)))
? That way you also get floating point and complex exponents for free.

-----

3 points by cchooper 6148 days ago | link

Because I couldn't find the expt function. D'oh!

I'll edit the above to use it.

Hmmm... where did the edit button go?

-----

4 points by absz 6148 days ago | link

Editing is time-limited; I can't decide if that's a good or a bad thing.

-----