Arc Forumnew | comments | leaders | submitlogin
Objects in Arc
5 points by EliAndrewC 5979 days ago | 26 comments
I've always liked the way that Javascript treats objects; they're just hashtables, and object methods are simply functions in that hashtable where the special value "this" refers to the hashtable.

For example, if we wanted a simple Person class, we'd say:

    function Person(first, last, age) {
        return {
            first: first,
            last:  last,
            age:   age,
            
            full: function() {
                return this.first + " " + this.last;
            },
            
            until: function(year) {
                return year - this.age;
            }
        }
    }
    
    p = new Person("Eli", "Courtwright", 25);
    p.full();
    p.until(30);
What I most like is that we can call methods on p instead of defining and calling global functions and passing p as a parameter (and thus not having to worry about name collisions).

For example, in Arc I'd do the same thing by saying

    (= p (obj
            first "Eli"
            last "Courtwright"
            age 25))
    
    (def person-full (p) (string p!first " " p!last))
    
    (def person-until (p year) (- year p!age))
    
    (person-full p)
    (person-until p 30)
I basically need to prefix my global functions with "person-" to avoid name collisions and so that it's clear what those functions are supposed to act upon. This isn't especially onerous, and maybe I'm being silly by letting this bother me, but I just really prefer "p.until(30)" to "(person-until p 30)". It both seems more aesthetically pleasing and easier to read.

So if we want to be able to write code like "(p!until 30)" then all we need is a macro:

    (mac new params
        `(let this (table)
            ,@(map (fn ((name val)) `(= (this ',name) ,val))
                   (pair params))
            this))
This basically gives us Javascript-style objects. We can use them like so:

    (def Person (first last age)
        (new
            first first
            last  last
            age   age
            full  (fn () (string this!first " " this!last))
            until (fn (year) (- year this!age))))
    
    (= p (Person "Eli" "Courtwright" 25))
    (p!full)
    (p!until 30)
This doesn't give us inheritance or many other features of "object-oriented" programming, but I still like it for letting me say "(p!until 30)" instead of "(person-until p 30)".

What do the more experienced Lispers out there think? Am I just having a beginner's reaction to Lisp, and should I just deal with "(person-until p 30)" which a module system might make more clear anyway? Or do many of you like the way that the "new" macro makes this code look?



4 points by conanite 5978 days ago | link

You can use lexical scoping to store private state for an object (not accessible via (= thing!foo 'bar) ), and so that your object's functions can call each other without requiring any kind of qualifying prefix. Here's a contrived example of a "shouter" that can be started, stopped, and shouts stuff when started.

  (def shouter (name)
    (with ((this started) nil)
      (= this (let (start stop shout prname equals) nil
                   (= start  (fn ()  (assert started)))
                   (= stop   (fn ()  (wipe started)))
                   (= shout  (fn (s) (if started (pr (upcase s)))))
                   (= prname (fn ()  (shout name)))
                   (= equals (fn (x) (is this x)))
                   (obj start start stop stop shout shout prname prname equals equals)))
       this))

  arc> (set sh1 (shouter "me"))
  arc> (sh1!start)
  arc> (sh1!prname)
  "ME"
"this" is defined so that 'equals will work (otherwise the functions have no way to reference the object that contains them); 'prname can call 'shout directly. 'name and 'started are hidden - 'name cannot be modified, and 'started can only change via the "public" 'start and 'stop functions.

The right macro can disappear all the boilerplate:

  (def shouter (name)
    (with ((this started) nil)
      (= this (make-obj
         (start  ()  (assert started))
         (stop   ()  (wipe started))
         (shout  (s) (if started (prn (upcase s))))
         (prname ()  (shout name))
         (equals (x) (is this x)) ))
    this))
'make-obj was inspired by obj and http://arclanguage.org/item?id=7387 ("if it's an idiom, it needs a macro") ...

  (with ((make-def double-each) nil)
    (= make-def (fn ((name args . body))
       `(def ,name ,args ,@body)))
    (= double-each (fn (lst)
       (if lst
           `(,(car lst) 
             ,(car lst) 
             ,@(double-each (cdr lst))))))
    (mac make-obj args
      `(with (,(map car args) nil)
             ,@(map make-def args)
             (obj ,@(double-each (map car args))))))
Coming from javaland, I fear I'm trying to hack objects onto arc and failing to see the One True Way with functions. But this macro has been convenient for interfacing with java. I guess some seriously tricky macroing would allow inheritance ... this is left as an exercise for the reader :)

-----

4 points by absz 5978 days ago | link

The real problem with storing the functions in the object, I think, is that you have different copies of the function for every object. There are two problems with this: (a) it wastes space, and (b) if you redefine the function, you have to recreate all your objects. That's the advantage of storing methods externally.

Also, speaking of "if it's an idiom, [it] probably needs a macro," I often find myself writing (obj name1 name1 name2 name2 name3 name3 ...), like you have above. So here's my nobj macro to solve that problem; for the same effect as above, just write (nobj name1 name2 name3 ...):

  (mac nobj args
    " Creates a table from the list of variables passed to it; each variable's
      name is keyed to its value.  E.g. if x = 10 and y = -10, then (nobj x y)
      results in #hash((y . -10) (x . 10)).
      See also [[obj]] [[table]] "
    `(obj ,@(flat:map [list _ _] args)))

-----

3 points by tokipin 5978 days ago | link

well, we can closure the methods with the constructor function so they aren't duplicated, at the expense of explicitly requiring the object to be passed to them, or some kind of dispatch mechanism. we can also implement simple inheritance:

  (def inherits (obj1 obj2)
       (fn (key)
           (or (obj1 key) (obj2 key))))
with (apply or ...) we could have a long inheritance chain, but here i have just two objects so the pattern is clear. we can then define objects like so:

  (let _proto (obj
                full (fn (self) (string self!first " " self!last))
                until (fn (self year) (- year self!age)))
  
    (def person (first last age)
           (inherits (obj
                       _proto _proto
                       first first
                       last last
                       age age)
  
                     _proto))
  
  )

  arc> (= p (person "joe" "momma" 18))
  #<procedure>
  arc> (p!full p)
  "joe momma"
  arc> (p!until p 22)
  4
  
  arc> (= q (person "axe" "murderer" 10))
  #<procedure>
  arc> (q!full q)
  "axe murderer"
  arc> (q!until q 22)
  12
because the inheritance is dynamically dispatched or whatitbe, we can alter the methods with the intended effects:

  arc> (q!backwards q)
  Error: "Function call on inappropriate object nil (#<procedure>)"
  arc> (= ((q '_proto) 'backwards) (fn (self) (string self!last " " self!first)))
  #<procedure:gs2439>
  arc> (q!backwards q)
  "murderer axe"
  arc> (p!backwards p)
  "momma joe"

  arc> (= ((q '_proto) 'until) (fn (self age) "a long time"))
  #<procedure:gs2451>
  arc> (q!until q 22)
  "a long time"
  arc> (p!until p 18)
  "a long time"
note the 'inherits' function is agnostic. we can inherit from arbitrary objects and functions or what have you, just that in this case it was used to inherit from a hidden prototype

anarki supposedly has user definable syntatic sugaries, so the (q!blah q) pattern could be sugarized

-----

2 points by almkglor 5978 days ago | link

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

-----

4 points by conanite 5976 days ago | link

Does it really store a new copy of the function each time? I thought it would only store the closure, and apparently closures are cheap ( http://arclanguage.org/item?id=7342 ) (sorry almkglor, I end up quoting you all the time).

And as for redefining functions: once your api is stable there's probably less need to redefine functions, and if necessary you can still

(= p!until (fn () ...))

(although this way you don't have access to "private" variables in the lexical scope of the original. I'm completely with EliAndrewC in preferring (p!until ...) over (person-until p ...)

And thanks for nobj, it's awesome. It makes my macro look like Visual Basic. Slowly, I learn ...

-----

3 points by almkglor 5976 days ago | link

Yes, a good implementation should store just the closed variables and a reference to the code - there shouldn't be any code duplication.

It thus depends on how many local variables are being closed over. Note that in some implementations (although not in arc2c, and by inference not in SNAP) a closure is just two pointers: a reference to an environment and a reference to the code. Of course each 'let form and function would create a new environment though, and this style is not so often used because lookup of closed variables can require indirection.

-----

2 points by absz 5976 days ago | link

You're welcome---I'm glad to have been of assistance.

As for storing copies, I would have said that it would store extra copies because of the different variable it closes over, but almkglor points out that you can separate code and environment, so the question is what mzscheme does.

The thing about redefinition is that in, say, Ruby, you can do

  class String
    def foo
      code_goes_here
    end
  end
And every string will have that new foo method. Here, you can only redefine the methods of one object.

For me, the real syntax question is whether we want (until p age) (which probably means that we are using the CLOS model of generic functions) or we want (p!until age) (which probably means that we are using the Smalltalk model of message passing). I sort of like the former syntax, but I also sort of prefer the Smalltalk model. What do you think?

-----

1 point by almkglor 5976 days ago | link

Note that redefinition using (p!until age) syntax is still possible in Arc using 'defcall and if you predeclare the private variables.

For instance, consider this:

  (deftype foo (x)
    (private y z)
    (meth niaw ()
      (do-something x y z))
    (meth arf (something)
      (do-something-else something x y z)))
  =>
  (let methods
       (table
         ; lambda lifted!
         'niaw
         (fn (x y z)
             (do-something x y z))
         'arf
         (fn (x y z something)
             (do-something-else something x y z)))
    (def foo-replace-method (s f)
      (= (methods s) f))
    ; so external code can determine the local variables
    (def foo-get-private-variables ()
      '(x y z))
    (def foo (x)
      (with (y nil z nil)
        (let invoker
             (fn (f rest)
               (apply f x y z rest))
        (fn (which-method)
          (aif
            (methods which-method)
               (fn rest (invoker it rest)))))))
Then a method redefining macro can be:

  (def lastcons (l)
    (if (cdr l)
        (lastcons:cdr l)
        l))
  (mac redef-meth (type meth params . body)
    (givens replacer (sym:string type "-replace-method")
            privates (eval:list:sym:string type "-get-private-variables")
            _ (= (cdr:lastcons privates) params)
      `(,replacer ,meth (fn ,privates ,@body))))
Note that foo-replace-method and foo-get-private-variables could be placed in a central global table or two instead.

-----

5 points by almkglor 5978 days ago | link

  (mac nobj args
    " Creates a table from the list of variables passed to it; each variable's
      name is keyed to its value.  E.g. if x = 10 and y = -10, then (nobj x y)
      results in #hash((y . -10) (x . 10)).
      See also [[obj]] [[table]] "
    `(obj ,@(mappend [list _ _] args)))
^^

-----

1 point by absz 5978 days ago | link

Even better, then :) And I definitely need to remember that this exists.

-----

3 points by almkglor 5978 days ago | link

^^ Of course you don't: just tell Arc about what you do remember, and it'll tell you more about related things you might want to look at too:

  arc> (help map)
  (from "arc.arc")
  [fn]  (map f . seqs)
   Applies the elements of the sequences to the given function.
      Returns a sequence containing the results of the function.
      See also [[each]] [[mapeach]] [[map1]] [[mappend]] [[andmap]]
      [[ormap]] [[reduce]]

-----

6 points by absz 5978 days ago | link

I know, but when was the last time you thought you needed help with map of all things? :)

Actually, aha! I added this to the bottom of ~/.arcshrc

  (let func (random-elt:keys help*)
    (prn "Documentation for " func " " (helpstr func)))
Now whenever I start arc, it will print, e.g.,

  Documentation for saferead (from "arc.arc")
  [fn]  (saferead arg)
   Reads an expression, blocking any errors. 
  
  Use (quit) to quit, (tl) to return here after an interrupt.
  arc> 
And thus hopefully I will learn something :)

-----

3 points by almkglor 5978 days ago | link

> 'make-obj was inspired by obj and http://arclanguage.org/item?id=7387 ("if it's an idiom, it needs a macro") ...

Lisp boy: Do not try to abstract the idiom. That's impossible. Instead... only try to realize the truth.

Newb: What truth?

Lisp boy: There is no idiom.

Newb: There is no idiom??

Lisp boy: Then you'll see, that it is not the idiom that does abstraction, but yourself.

^^

-----

3 points by absz 5979 days ago | link

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

  (mac obj args
    (w/uniq g
      `(let ,g (table)
         ,@(map (fn ((k v)) `(= (,g ',k) ,v))
                (pair args))
         ,g)))
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.

-----

2 points by EliAndrewC 5978 days ago | link

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.

-----

3 points by absz 5978 days ago | link

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.

-----

1 point by EliAndrewC 5978 days ago | link

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.

-----

2 points by absz 5978 days ago | link

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.

-----

3 points by tokipin 5979 days ago | link

i'd do it something like:

  (def person (first last age)
       (let self nil
         (= self (obj
           first first
           last last
           age age
           full (fn () (string self!first " " self!last))
           until (fn (year) (- year self!age))))))

-----

1 point by EliAndrewC 5979 days ago | link

Wow, that's much better! I didn't think that would work, since I thought that Arc's lexical scope bound variables in inner functions to the current value of those variables. Apparently I was wrong; thanks for the tip.

-----

2 points by bOR_ 5979 days ago | link

From another beginner ;). Isnt this what templates are for? Here is a template I am using:

  ; Template of a creature
  (deftem creature 
   symbol #\@
   dir '(1 1) 
   pos '(0 0)

   ;memory bits
   stm (n-of 7 nil)
   oltm (table) ; ltm - the objects associated with a
   altm (table) ; ltm - the actions associated with a
   cltm (table) ; ltm - the emotions associations with a
   ltmsize 25

   s2ltm 3

   ;list of behaviours/ actions
   action (list act_bite)

   ; observations:
   fov 110  ; fov not being exactl fov, but half-fov
   range 7
   hunger 0
   )
and now I can just call an instance of this template, including its default values. I basically use that as my objects.

  (= craa (inst 'creature))

-----

2 points by EliAndrewC 5979 days ago | link

Does this give you any kind of "this" object in your object methods? That's a large part of what I'm looking for, since that lets me say "(p!until 30)" in my example, or something like "(craa!act_bite)" in your example.

-----

2 points by gnaritas 5979 days ago | link

Wouldn't that be...

    function Person(first, last, age) {
        this.first = first;
        this.last = last;
        this.age = age;
        this.full = function() {
            return this.first + " " + this.last;
        };
        this.until = function(year) {
            return year - this.age;
        };
    }
There's really no reason return an anonymous literal object from the Person constructor unless I'm overlooking something, it has been a year or two since I played with JS much.

-----

1 point by EliAndrewC 5979 days ago | link

You are correct; it's also been awhile since I played with Javascript, but I just checked "A Reintroduction to Javascript" then it listed both my way and your way as examples, describing your way as being cleaner: http://developer.mozilla.org/en/docs/A_re-introduction_to_Ja...

-----

2 points by tung 5975 days ago | link

I'd do it like this:

    function Person(first, last, age) {
        this.first = first;
        this.last = last;
        this.age = age;
    }
    
    Person.prototype.full = function () {
        return this.first + " " + this.last;
    };
    
    Person.prototype.until = function (year) {
        return year - this.age;
    };
You shouldn't really put functions inside the constructor function unless you really need the closure.

-----

1 point by gnaritas 5969 days ago | link

Which is handy if you want private instance variables. But you're right, prototype should be the first choice, like I said, it's been a while.

-----