I'm not sure what I think of the coding style, but there is a problem with your new macro. What would happen, for instance, if you had
(let this nil
(new push [push _ this] ; Anonymous function with an argument named _
pop (fn () (pop this))))
When we expand new, we get
(let this nil
(let this (table)
(= (this 'push) [push _ this])
(= (this 'pop) (fn () (pop this)))
this))
Now, your stack no longer works---it's modifying the wrong this! Moral of the story: never let names be bound in macros. The solution is to use a unique name, generated by the function uniq. The macro w/uniq lets you get as many uniqs as you need. To rewrite new using this, we would write
Notice how instead of this, you have ,g (which must be unquoted because g holds the name we want). And also notice how I've changed some names---that's because this is the obj macro from vanilla Arc. So moral of the story number two: use built-in Arc functions (the caveat, of course, is that the documentation is, shall we say, subpar, so good luck).
Your examples will all work, thus, if you change new to obj. As for the style... I agree that person-foo is ugly, but p!foo doesn't really feel right either. One solution is to switch to Anarki (http://arclanguage.org/item?id=4951) and use a combination of annotate, defcall, and nex3's defm macro (http://arclanguage.org/item?id=4644). Here's how this might work:
(def person (first last age)
; (annotate 'type object) creates an object of type 'type.
; The object's type is obtained by (type o), and the object's
; representation (the second argument) is obtained by (rep o).
(annotate 'person
(obj first first
last last
age age)))
; When an object of type 'person is used as a "function", e.g.
; p!first, this will be called (where self is p and field is 'first).
; Insert error checking if you want p!nonfield not to return nil.
(defcall person (self field)
self.field)
; See http://arclanguage.org/item?id=4644 for documentation of defm.
(defm full ((t self person))
(string ((rep self) 'first) " " ((rep self) 'last))
(defm until ((t self person) year)
(- year ((rep self) 'age))
; Examples
(= p (person "Eli" "Courtwright" 25))
(prn p!first)
(prn:full p)
(prn:until p 30)
This is a bit cumbersome, so you could define abstracting macros. Some of these already exist, for instance <plug>my tagged union system on Anarki (http://arclanguage.org/item?id=7364)</plug>, which would turn this code into
(vtype person (first string?)
(last string?)
(age (andf int? [>= _ 0])))
; And the same defms and examples from above.
Of course, this provides you with all sorts of nifty features; see the link for details. And I recall seeing other proposed object systems too, but I can't help you with those.
I actually used "obj" as a reference when writing "new", which is why they were so similar. I decided not to use a gensym because I figured that "this" would effectively be a reserved word with a common meaning and so that I could use "this" in my methods. However, your point about clashes is well taken.
As for "defm", it looks pretty nice. I guess I wouldn't mind calling "(until p 30)" instead of "(p!until 30)" so long as I don't have to call "person-until" just to prevent naming clashes.
I really dislike having to call "((rep self) 'first)" instead of "self!first", but as you point out, I might be able to define some macros to make this less verbose.
I guess for now I'll start playing around with Anarki. Hopefully the more knowledgable Lispers out there will settle on an object system that's general enough to be useful and concise enough to by enjoyable.
Ah, I see -- new is an auto-binding obj. That makes sense if you're including methods (which perhaps you shouldn't). It's just that usually when people do that it is a mistake :)
Actually, that was a mistake on my part; you can in fact write
(defm full ((t self person))
(string self!first " " self!last)
(defm until ((t self person) year)
(- year self!age)
because the defcall allows you to put objects of type person in that position. And now the only redundancy left is having to write (t self person) all the time, which you may or may not want to abstract.
The other missing feature is the inability to say (= p!age 42); for that, you need to override sref:
; Insert error checking so that you can't
; write (= p!species 'banana).
(defm sref ((t self person) value key)
(= ((rep self) key) value))
Again, <plug>the tagged unions do all of this for you (except for defining your own methods, which you have to do with defm and vcase/tcase)</plug>. Of course, they don't have inheritance.
That sounds excellent, and it makes me a lot more excited about using objects in Arc. I don't care that much about having to write (t self person) a lot, since it seems like an acceptably low amount of boilerplate.
However, out of curiosity, why do you have to write "t" instead of just saying "(self person)"? I read through the code for defm and argmap, but my Lisp reading skills aren't strong enough to decipher it.
Because parameter lists in Arc support destructuring. What that means is that anywhere you can write variable to bind a name to a value (such as in (let variable 10 (prn variable))), you can also write (v1 v2) to bind a list's first element to v1 and second element to v2. And (o v default) denotes optional parameters. Perhaps some examples would be clearer:
(with (i 1 v 5 x 10)
(let a (list i v) (prn "a is (1 5)."))
(let (b c) (list i v) (prn "b is 1 and c is 5."))
(let (d . e) (list i v x) (prn "d is 1 and e is (5 10)."))
(let (f (o g)) (list i) (prn "f is 1 and g is nil."))
(let (h (o j 42)) (list i) (prn "h is 1 and j is 42."))
(let (k (o m)) (list i v) (prn "k is 1 and m is 5."))
(let (n (o p 42)) (list i v) (prn "n is 1 and p is 5.")))
defm adds a (t var type) syntax; if you left out the t, you would have ordinary destructuring bind.