Arc Forumnew | comments | leaders | submitlogin
Class macros
1 point by ylando 5290 days ago | 13 comments
I have an interesting idea to make macros object oriented.

  Pros: It can shorten the macro names.
        It can combine macros with classes.
        It can create a shortcut for a big if statment.
  Cons: we can not do polymorphism.
It is implemented in the following code:

  (= class-macro-table* (obj)) ;A hash table of class functions
  (= declare-obj-class* (obj)) ;A hash table of temprorary obj annotations
  ; A variable that exist becouse every macro must return a value.
  (= temporary* nil)
  
  ; declare an object type
  (mac dec (obj types . body) 
  (let oldtype (declare-obj-class* obj)
       (= (declare-obj-class* obj) types)
         `(do (= temporary* (do ,@body)) (undec ,obj ,oldtype))))

  ; remove object declaration
  (mac undec (obj oldtype) 
  (= (declare-obj-class* obj) oldtype)
   'temporary*)

  ; Creating a class macro 
  (mac cmac (funcname classname args . body)
   (with (realName (uniq) args2 (uniq)) 
     (unless class-macro-table*.classname 
       (= class-macro-table*.classname (obj)))
     (= class-macro-table*.classname.funcname realName) 
     (let realmac `(mac ,realName (,classname ,@args) ,@body)
       (if (bound funcname)
         realmac
         `(do ,realmac (mac ,funcname ,args2 
     (apply _callmac ',funcname (car ,args2) (cdr ,args2)))) ))))


  ; Helper function for calling a macro
  (def _callmac2 (mac-name obj-name . args)
  (if (aand class-macro-table*.obj-name it.mac-name) 
    (let macro-real-name class-macro-table*.obj-name.mac-name
       `(,macro-real-name ,obj-name ,@args))  
    (err "Undeclare macro for this object")))

  (def _callmac (mac-name obj-name . args)
    (let obj-class declare-obj-class*.obj-name
      (if  (no obj-class) (err "Undeclare object")
           (acons obj-class) 
              (let if-expr ()
                 (each class obj-class 
                      (do
               (push (apply _callmac2 mac-name class args) if-expr) 
               (push `(isa ,obj-name ',class) if-expr)))
                       
                 `(if ,@if-expr (err "Undeclare object")))
           (apply _callmac2 mac-name obj-class args))))

  ; macro for calling class macros
  (mac callmac (mac-name obj-name . args) 
     (apply _callmac mac-name obj-name args))

Class macro usage:

  Suppose we have three types of messages:
  sms-msg voice-msg and email-msg
  we can declare a w/msg macro like this:

  (cmac w/msg sms-msg (arg1 arg2 ...) ... )
  (cmac w/msg voice-msg (...) ...)
  (cmac w/msg email-msg (...) ...)
If we want to call a macro in a function, we write:

  (dec email-msg-obj email-msg
    (w/msg email-msg-obj ...))
If an object can be one of the three messages, It can generate if statement, in the following way:

  (dec msg-obj (sms-msg voice-msg email-msg)
    (w/msg msg-obj ...))
What do you think about class macros?


3 points by fallintothis 5281 days ago | link

I was confused about how it was supposed to work, so I reimplemented it to learn. This is what I came up with.

  (= declarations* (table))

  (mac dec (name types . body)
    (let old (declarations* name)
      (= (declarations* name) types)
      `(after (do ,@body)
         (= (declarations* ',name) ',old))))

  (= methods* (table))

  (mac macmethod (name class args . body)
    (or= (methods* class) (table))
    (w/uniq (new-method parms)
      (= methods*.class.name new-method)
      `(do (mac ,new-method (,class ,@args) ,@body)
           (unless (bound ',name)
             (mac ,name ,parms
               (invoke ',name (car ,parms) (cdr ,parms)))))))

  (def invoke (method object args)
    (let classes (declarations* object)
      (if (no classes)
           (err "Undeclared object:" object)
          (acons classes)
           `(case (type ,object)
              ,@(mappend [list _ (invoke1 method _ args)]
                         classes)
              (err (+ "No method " ',method " for type " (type ,object))))
           (invoke1 method classes args))))

  (def invoke1 (method class args)
    (or (aand methods*.class
              it.method
              `(,it ,class ,@args))
        `(err (+ "No method " ',method " for class " ',class))))
It's not a one-to-one translation, but it brought some oddities to the surface.

There's a disconnect between the single declarations (dec x foo ...) and the multi-declarations (dec x (foo bar baz) ...). E.g., with

  (cmac w/msg sms   () `(prn "dispatched on sms"))
  (cmac w/msg voice () `(prn "dispatched on voice"))
  (cmac w/msg email () `(prn "dispatched on email"))
We can do

  arc> (dec x voice (w/msg x))
  dispatched on voice
  "dispatched on voice"
but

  arc> (dec x (sms voice email) (w/msg x))
  Error: "reference to undefined identifier: _x"
This is because

  (dec x voice (w/msg x))
only looks at

  (declare-obj-class* 'x)
to decide what to do, but

  (dec x (sms voice email) (w/msg x))
treats x as a variable and dispatches on (type x) -- in my implementation, a case, in yours, an if with a bunch of isas. Using type for arbitrary "classes" won't do so well in Arc as it stands, because custom types aren't used very liberally. But, it's possible.

  arc> (= x (annotate 'sms 'blah))
  #(tagged sms blah)
  arc> (dec x (sms voice email) (w/msg x))
  dispatched on sms
  "dispatched on sms"
Because of this expansion (into a case or if), I took the liberty of quasiquoting the "Undeclare[d] macro for this object" error in my rewrite.

Yours:

  arc> (type x)
  sms
  arc> (dec x (sms nonexistant-class) (w/msg x))
  Error: "Undeclare macro for this object"
  arc> (dec x (foo bar baz) (w/msg x))
  Error: "Undeclare macro for this object"
  arc> (dec x foo (w/msg x))
  Error: "Undeclare macro for this object"
  arc> (dec x sms (w/msg x))
  dispatched on sms
  "dispatched on sms"
Mine:

  arc> (type x)
  sms
  arc> (dec x (sms nonexistant-class) (w/msg x))
  dispatched on sms
  "dispatched on sms"
  arc> (dec x (foo bar baz) (w/msg x))
  Error: "No method w/msg for type sms"
  arc> (dec x foo (w/msg x))
  Error: "No method w/msg for class foo"
  arc> (dec x sms (w/msg x))
  dispatched on sms
  "dispatched on sms"
I'm still confused that the macros take in some sort of implicit parameter which is (I think) invariably just the class being dispatched upon. Is it good for anything?

Yours:

  arc> (cmac m a (x y z)
         `(do1 nil
               (prs "dispatching on" ',a "with args" ',x ',y ',z #\newline)))
  #(tagged mac #<procedure: m>)
  arc> (cmac m b (x y z)
         `(do1 nil
               (prs "dispatching on" ',b "with args" ',x ',y ',z #\newline)))
  #(tagged mac #<procedure: gs1774>)
  arc> (dec foo a (m foo bar baz quux))
  dispatching on a with args bar baz quux
  nil
  arc> (= foo (annotate 'b 'foo))
  #(tagged b foo)
  arc> (dec foo (a b) (m foo bar baz quux))
  dispatching on b with args bar baz quux
  nil
Mine:

  arc> (macmethod m a (x y z)
         `(do1 nil
               (prs "dispatching on" ',a "with args" ',x ',y ',z #\newline)))
  #(tagged mac #<procedure: m>)
  arc> (macmethod m b (x y z)
         `(do1 nil
               (prs "dispatching on" ',b "with args" ',x ',y ',z #\newline)))
  nil
  arc> (dec foo a (m foo bar baz quux))
  dispatching on a with args bar baz quux
  nil
  arc> (= foo (annotate 'b 'foo))
  #(tagged b foo)
  arc> (dec foo (a b) (m foo bar baz quux))
  dispatching on b with args bar baz quux
  nil
The fact that the "methods" are macros also means that the multi-declaration case will try to expand every declared possibility, regardless of what type the object is.

  arc> (cmac incongruent-args a () `(list ',a))
  #(tagged mac #<procedure: incongruent-args>)
  arc> (cmac incongruent-args b (x) `(list ',b ',x))
  #(tagged mac #<procedure: gs1774>)
  arc> (cmac incongruent-args c (x y) `(list ',c ',x ',y))
  #(tagged mac #<procedure: gs1776>)
  arc> (dec foo a (incongruent-args foo))
  (a)
  arc> (dec foo b (incongruent-args foo bar))
  (b bar)
  arc> (dec foo c (incongruent-args foo bar baz))
  (c bar baz)
  arc> (= foo (annotate 'c 'foo))
  #(tagged c foo)
  arc> (dec foo (c) (incongruent-args foo bar baz))
  (c bar baz)
  arc> (dec foo (a b c) (incongruent-args foo bar baz))
  Error: "procedure  gs1774: expects 2 arguments, given 3: b bar baz"
  arc> (dec foo (c b a) (incongruent-args foo bar baz))
  Error: "procedure  gs1772: expects 1 argument, given 3: a bar baz"
because, in yours, it expands into:

  arc> (= (declare-obj-class* 'foo) '(a b c))
  (a b c)
  arc> (ppr:_callmac 'incongruent-args 'foo 'bar 'baz)
  (if (isa foo 'c)
      (gs1776 c bar baz)
      (isa foo 'b)
      (gs1774 b bar baz)
      (isa foo 'a)
      (gs1772 a bar baz)
      (err "Undeclare object"))t
and each gs... macro gets expanded anyway -- sometimes with the wrong number of arguments.

Same thing in mine:

  arc> (= (declarations* 'foo) '(a b c))
  (a b c)                               
  arc> (ppr:invoke 'incongruent-args 'foo '(bar baz))
  (case (type foo)
    a
    (gs1791 a bar baz)
    b
    (gs1793 b bar baz)
    c
    (gs1795 c bar baz)
    (err (+ "No method "
            'incongruent-args
            " for type "
            (type foo))))t
In general, I'm not sure I've ever needed "object-oriented" macros (especially in Arc), but I've never really looked for a use-case.

-----

1 point by ylando 5281 days ago | link

I have a use case. If we have an object system and we want to implement the "each" macro for every collection.

-----

1 point by evanrmurphy 5281 days ago | link

  (w/speculation
Much of Lisp's power stems from the fact that virtually everything can be represented as a list. If this is true, then writing 'each for lists is almost as good as (or better than) writing a generic 'each for every kind of type. A language design like Arc's that capitalizes on this idea indirectly provides incentive for making as many things into lists as possible. This is why pg has flirted with representing both strings [1] and numbers [2] as lists, and also why he promotes using assoc lists over tables when possible [3].

One disadvantage of this approach is that it can sometimes seem unnatural to represent x as a list, but it has the benefit of providing a very minimal cloud of abstractions with maximum flexibility.

A powerful object system seems like a different way of going about the same thing. That's probably why Lisp users are often unenthusiastic about objects: there's a feeling of redundancy and of pulling their language in two different directions. (Arc users can be especially unenthusiastic because they're so anal about minimizing the abstraction cloud.) That's why I'm not particularly enthusiastic about objects, anyway. They're not worse - just different, and largely unnecessary if you have lists.

I could be missing the ship here. I don't have enough experience with object systems to understand all their potential benefits over using lists for everything. (And, of course, the popularity of CLOS demonstrates that a lot of people like to have both!)

  )
[1] arc.arc has this comment toward the top:

  ; compromises in this implementation: 
  ...
  ; separate string type
  ;  (= (cdr (cdr str)) "foo") couldn't work because no way to get str tail
  ;  not sure this is a mistake; strings may be subtly different from 
  ;  lists of chars
[2] See this quote from http://www.paulgraham.com/hundred.html and its subsequent paragraphs:

The Lisp that McCarthy described in 1960, for example, didn't have numbers. Logically, you don't need to have a separate notion of numbers, because you can represent them as lists: the integer n could be represented as a list of n elements. You can do math this way. It's just unbearably inefficient.

[3] See "Assoc-lists turn out to be useful." in http://www.paulgraham.com/arclessons.html and

  I once thought alists were just a hack, but there are many things you can
  do with them that you can't do with hash tables, including sort
  them, build them up incrementally in recursive functions, have
  several that share the same tail, and preserve old values.
from http://ycombinator.com/arc/tut.txt.

-----

4 points by rocketnia 5280 days ago | link

I think you're right about it being frustrating to be pulled in multiple directions when choosing how to represent a data structure.

In Groovy, I'm pulled in one direction:

  class Coord { int x, y }
  ...
  new Coord( x: 10, y: 20 )
    + okay instantiation syntax
    + brief and readable access syntax: foo.x
As the project evolves, I can change the class definition to allow for a better toString() appearance, custom equals() behavior, more convenient instantiation, immutability, etc.

In Arc, I'm pulled in about six directions, which are difficult to refactor into each other:

  '(coord 10 20)
    + brief instantiation syntax
    + brief write appearance: (coord 10 20)
    + allows (let (x y) cdr.foo ...)
    - no way for different types' x fields to be accessed using the same
        code without doing something like standardizing the field order
  
  (obj type 'coord x 10 y 20)
    + brief and readable access syntax: do.foo!x (map !x foos)
    + easy to supply defaults via 'copy or 'deftem/'inst
  
  [case _ type 'coord x 10 y 20]
    + immutability when you want it
    + brief and readable access syntax: do.foo!x (map !x foos)
    - mutability much more verbose to specify and to perform
  
  (annotate 'coord '(10 20))
    + easy to use alongside other Arc types in (case type.foo ...)
    + semantically clear write appearance: #(tagged coord (10 20))
    + allows (let (x y) rep.foo ...)
    - no way for different types' x fields to be accessed using the same
        code without doing something like standardizing the field order
  
  (annotate 'coord (obj x 10 y 20))
    + easy to use alongside other Arc types in (case type.foo ...)
    + okay access syntax: rep.foo!x (map !x:rep foos)
  
  (annotate 'coord [case _ x 10 y 20])
    + immutability when you want it
    + easy to use alongside other Arc types in (case type.foo ...)
    + okay access syntax: rep.foo!x (map !x:rep foos)
    - mutability much more verbose to specify and to perform
(This doesn't take into account the '(10 20) and (obj x 10 y 20) forms, which for many of my purposes have the clear disadvantage of carrying no type information. For what it's worth, Groovy allows forms like those, too--[ 10, 20 ] and [ x: 10, y: 20 ]--so there's no contrast here.)

As the project goes on, I can write more Arc functions to achieve a certain base level of convenience for instantiation and field access, but they won't have names quite as convenient as "x". I can also define completely new writers, equality predicates, and conditional syntaxes, but I can't trust that the new utilities will be convenient to use with other programmers' datatypes.

In practice, I don't need immutability, and for some unknown reason I can't stand to use 'annotate and 'rep, so there are only two directions I really take among these. Having two to choose from is a little frustrating, but that's not quite as frustrating as the fact that both options lack utilities.

Hmm, that gives me an idea. Maybe what I miss most of all is the ability to tag a new datatype so that an existing utility can understand it. Maybe all I want after all is a simple inheritance system like the one at http://arclanguage.org/item?id=11981 and enough utilities like 'each and 'iso that are aware of it....

-----

1 point by shader 5279 days ago | link

I rewrote the type system for arc a while ago, so that it would support inheritance and generally not get in the way, but unfortunately I haven't had the time to push it yet. If you're interested, I could try to get that up some time soon.

-----

1 point by rocketnia 5279 days ago | link

Well, I took a break from wondering what I wanted, and I did something about it instead, by cobbling together several snippets I'd already posted. So I'm going to push soon myself, and realistically I think I'll be more pleased with what I have than what you have. For instance, Mine is already well-integrated with my multival system, and it doesn't change any Arc internals, which would complicate Lathe's compatibility claims.

On the other hand, and at this moment it's really clear to me that implementing generic doppelgangers of arc.arc functions is a bummer when it comes to naming, and modifying the Arc internals to be more generic, like you've done (right?), could really make a difference. Maybe in places like that, your approach and my approach could form an especially potent combination.

-----

2 points by rocketnia 5266 days ago | link

I finally pushed this to Lathe. It's in the new arc/orc/ folder as two files, orc.orc and oiter.arc. The core is orc.arc, and oiter.arc is just a set of standard iteration utilities like 'oeach and 'opos which can be extended to support new datatypes.

The main feature of orc.arc is the 'ontype definition form, which makes it easy to define rules that dispatch on the type of the first argument. These rules are just like any other rules (as demonstrated in Lathe's arc/examples/multirule-demo.arc), but orc.arc also installs a preference rule that automatically prioritizes 'ontype rules based on an inheritance table.

It was easy to define 'ontype, so I think it should be easy enough to define variants of 'ontype that handle multiple dispatch or dispatching on things other than type (like value [0! = 1], dimension [max { 2 } = 2], or number of arguments [atan( 3, 4 ) = atan( 3/4 )]). If they all boil down to the same kinds of rules, it should also be possible to use multiple styles of dispatch for the same method, resolving any ambiguities with explicit preference rules. So even though 'ontype itself may be limited to single dispatch and dispatching on type, it's part of a system that isn't.

Still, I'm not particularly sure orc.arc is that helpful, 'cause I don't even know what I'd use it for. I think I'll only discover its shortcomings and its best applications once I try using it to help port some of my Groovy code to Arc.

http://github.com/rocketnia/lathe

-----

1 point by shader 5279 days ago | link

Well, I guess we'll just have to find out ;)

And yes, I did modify arc's internals to be more generic. Basically, I replaced the vectors pg used for typing with lists and added the ability to have multiple types in the list at once. Since 'type returns the whole list, and 'coerce looks for conversion based on each element in order, we get a simple form of inheritance and polymorphism, and objects can be typed without losing the option of being treated like their parents.

-----

3 points by rocketnia 5281 days ago | link

For that example, what about just defining a 'walk method for every collection and defining 'each the way Anarki does?

  (mac each (var expr . body)
    `(walk ,expr (fn (,var) ,@body)))

-----

1 point by ylando 5280 days ago | link

"Each" can iterate over millions of elements; in this case, we can use class macros to remove the function calls.

-----

2 points by fallintothis 5281 days ago | link

If we have an object system

Which explains why I've never needed object-oriented macros. :)

-----

1 point by ylando 5281 days ago | link

I have a solution to a small problem with the code. Put:

  (def macex-all (e)
    (zap macex e)
    (if acons.e
        (map1 macex-all e)
        e))

  (mac dec (obj type . body)
    (let oldtype (declare-obj-class* obj) 
       (= (declare-obj-class* obj) type)  
       (let exp-body (macex-all body)
            (= (declare-obj-class* obj) oldtype)
            (cons 'do exp-body))))
and remove undec and temporary*. This fixes the problem of declaration wasting run time.

-----

4 points by fallintothis 5281 days ago | link

That version of macex-all is a bit primitive.

  arc> (macex-all '(fn (do re mi) (+ do re mi)))
  (fn ((fn () re mi)) (+ do re mi))
  arc> (macex-all ''(do not expand this -- it is quoted))
  (quote ((fn () not expand this -- it is quoted)))
If you want a more robust macex-all, you can gank my implementation from http://arclanguage.org/item?id=11806 (rewritten slightly):

  (def imap (f xs)
    (when xs
      (if (acons xs)
          (cons (f (car xs)) (imap f (cdr xs)))
          (f xs))))

  (def macex-all (expr)
    (zap macex expr)
    (check expr
           atom
           (case (car expr)
             quasiquote (list 'quasiquote
                              ((afn (level x)
                                 (if (is level 0)
                                      (macex-all x)
                                     (caris x 'quasiquote)
                                      (list 'quasiquote (self (+ level 1) cadr.x))
                                     (in acons&car.x 'unquote 'unquote-splicing)
                                      (list car.x (self (- level 1) cadr.x))
                                      (check x atom (imap [self level _] x))))
                               1 (cadr expr)))
             fn         `(fn ,(cadr expr) ,@(imap macex-all (cddr expr)))
             quote      expr
                        (imap macex-all expr))))
This also won't break on dotted lists, like

  arc> (macex-all '`(a . b))
  Error: "Can't take car of b"
But if you don't care about that, you can replace all the uses of imap with map1 or map.

Also, a couple nitpicks about dec.

  (macex-all body)
breaks, because body is a list, so if its car is a macro, macex-all thinks it's a macro call.

  arc> (ppr:macex1 '(dec foo bar
                      mac baz quux quack))
  (do (fn ()
        (sref sig 'quux 'baz)
        ((fn ()
           (if (bound 'baz)
               ((fn ()
                  (disp "*** redefining " (stderr))
                  (disp 'baz (stderr))
                  (disp #\newline (stderr)))))
           (assign baz
                   (annotate 'mac (fn quux quack)))))))t
Notice too that consing do to the front breaks in this case. So, you should use

  (map1 macex-all body)
You won't need to care about dotted lists, since body will be proper (rest parameters in Arc always are).

Finally, the pattern

  (let result x
    ... stuff not involving x or result ...
    result)
is exactly what do1 is for. Altogether, that's

  (mac dec (obj type . body)
    (let oldtype (declare-obj-class* obj)
      (= (declare-obj-class* obj) type)
      (do1 (cons 'do (map1 macex-all body))
           (= (declare-obj-class* obj) oldtype))))

-----