Arc Forumnew | comments | leaders | submitlogin
3 points by fallintothis 5028 days ago | link | parent

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 5028 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 5027 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 5026 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 5026 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 5026 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 5013 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 5026 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 5027 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 5027 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 5028 days ago | link

If we have an object system

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

-----