Arc Forumnew | comments | leaders | submitlogin
Implementing prototypes in Arc
4 points by Pauan 5153 days ago | 4 comments
For those of you who don't know, JavaScript uses prototypes rather than classes like most popular languages. What this boils down to is that class and instance is the same thing. Rather than creating a class that inherits from another class, and then creating an instance, you simply create an object that inherits directly from another object.

In my opinion, this unification of class and instance leads to a simpler and more light-weight inheritance system while still retaining the power of classes. Although Arc is not an OO language, I think prototypes can have their uses, for instance to save memory in certain situations.

How it works is: when looking up a property, if the property is not found, it then checks it's parent. This is applied recursively until either the property is found, or not found. I implemented a "clone" macro that does just that. Here's an example:

  (= a (clone nil x "foo"))
  (= b (clone a   y "bar"))
  (= c (clone b   z "qux"))
  
  a!x -> "foo"
  a!y -> nil
  a!z -> nil
  
  b!x -> "foo"
  b!y -> "bar"
  b!z -> nil
  
  c!x -> "foo"
  c!y -> "bar"
  c!z -> "qux"
(clone nil) is the same as (obj) except that "clone" is probably slower. Now, it's important to understand that this does not copy the object. Observe:

  (= b!x "changed")

  a!x -> "foo"
  b!x -> "changed"
  c!x -> "changed"
By changing a property on "b", it caused the property to be changed on "c" as well, because it inherits from "b". Here is the entire implementation. It uses aw's extend macro, which makes it trivial to allow for different data-types in a call to "=":

  ;; http://awwx.ws/extend

  (extend sref (x v n) (isa x 'fn) (x n v))

  (mac clone (p . args)
    (w/uniq new
     `(let ,new (obj ,@args)
        (fn (n . args)
          (if args (= (,new n) (car args))
                   (or (,new n) (and ,p (,p n))))))))
As a consequence of extending sref, functions can now be used as setters when they are assigned to with "=". What does this mean? Perhaps an example will help:

  (def foo (name value)
    (prn:list name value))

  (= foo!x "bar") -> (x bar)
As you can see, the function "foo" is called and is given both the variable that is being assigned to, and the value. This is the secret to making "clone" work. Can anybody think of any situations where this overloading would cause problems?

P.S. This simple system does not handle types correctly, since "clone" returns a function. So for instance, the "keys" function won't work. I'm not sure how to get that working, but perhaps this simple inheritance is useful in some circumstances.

Also, supplying a default parameter causes the value to be written: (a 'x 10) This is an inconsistency with (obj), which does not write the value. I may be able to fix this.

P.P.S. I was surprised by how trivial it was to both extend sref and implement clone. Arc's expressive power seems quite high, to me.



3 points by fallintothis 5153 days ago | link

Can anybody think of any situations where this overloading would cause problems?

Admittedly, I can't think of any, but the scope of the change makes me uncomfortable. For any function f, (sref f v k) turns into (f k v)? Seems heavy-handed.

An alternative is to use defset to special-case on prototypes, but that relies on knowing the name of your prototype object. So, you'd have to make the macro explicitly assign to a name and defset that, which is kind of ugly:

  (mac defproto (name parent . attrs)
    (w/uniq lookup
      `(let ,lookup (obj ,@attrs)

         (= ,name (fn (attr . args)
                    (if args
                        (= (,lookup attr) (car args))
                        (or (,lookup attr)
                            ((only .attr) ,parent)))))

         (defset ,name (attr)
           (w/uniq g
             (list (list g attr)
                   `(,',name ,g)
                   `(fn (val) (,',name ,g val)))))

         ,name)))

  arc> (defproto a nil x "foo")
  #<procedure: a>
  arc> (defproto b a   y "bar")
  #<procedure: b>
  arc> (defproto c b   z "qux")
  #<procedure: c>
  arc> (map [list _!x _!y _!z] (list a b c))
  (("foo" nil nil) ("foo" "bar" nil) ("foo" "bar" "qux"))
  arc> (= b!x "changed")
  "changed"
  arc> (list a!x b!x c!x)
  ("foo" "changed" "changed")
By separating out the setter logic, you could address the (a 'x 10) issue:

  (mac defproto (name parent . attrs)
      (w/uniq (lookup attr-setter)
        `(let ,lookup (obj ,@attrs)

           (= ,name
              (fn (attr (o default))
                (or (,lookup attr default)
                    ((only [_ attr default]) ,parent))))

           (= ,attr-setter
              (fn (attr val)
                (= (,lookup attr) val)))

           (defset ,name (attr)
             (w/uniq g
               (list (list g attr)
                     `(,',name ,g)
                     `(fn (val) (,',attr-setter ,g val)))))

           ,name)))

  arc> (defproto a nil x 5)
  #<procedure:zz>
  arc> (a 'x)
  5
  arc> (a 'x 10)
  5
  arc> (a 'y)
  nil
  arc> (a 'y 10)
  10
  arc> (a 'y)
  nil
I'm not sure how to address the issue of using fns as protoypes. As Arc's type system stands, there may not be a much better way.

-----

1 point by Pauan 5152 days ago | link

Hm... looks like I won't be able to transparently integrate it into Arc without changing ac.scm. I could try using defcall from Anarki, though.

-----

2 points by rocketnia 5153 days ago | link

What you think of as prototype inheritance, I usually think of as scope shadowing. :) Here's some untested code to show how I'd go about what you're doing:

  ; This kind of table can distinguish between nil and undefined.
  (def singleton-table args
    (when (odd len.args)
      (err "Can't pair the args to 'singleton-table."))
    (listtab:map [list _.0 (list _.1)] pair.args))
  
  (def proto (parent . binds)
    (annotate 'proto
      (list (apply singleton-table binds) parent)))
  
  (= empty-proto* (annotate 'proto nil))
  
  (def proto-get-maybe (self field)
    (whenlet (binds parent) rep.self
      (or binds.field (proto-get-maybe parent field))))
  
  ; Here I use 'defcall, which is defined in Anarki. Rainbow has a
  ; variant too.
  (defcall proto (self field)
    (car:or (proto-get-maybe self field)
      (err:+ "No field \"" (tostring write.field) "\" on "
             (tostring write.self) ".")))
  
  (extend sref (self val . args) (and (isa self 'proto) single.args)
    (= (rep.self.0 car.args) list.val))
Actually, I'd get by with the longhand functions 'proto-get and 'proto-set, but that's not as nice. ^^ I only do that because I try to support plain Arc 3.1 and Jarc, which don't have 'defcall. (Supporting Jarc is nice 'cause of its debugger, and supporting Arc 3.1 is nice 'cause it's the easiest to talk about. Anarki has the most community involvement, and Rainbow is fast.)

-----

2 points by Pauan 5150 days ago | link

I didn't really think of prototypes in the same way as scope, but you're right that they're basically the same thing. I suppose you could say that prototypical inheritance is scope applied to objects, rather than functions.

With that in mind, I'm curious whether prototypes would even be useful in Arc. I don't use them much in JavaScript, preferring to use plain old objects/variables/closures.

In any case, this was more of a theoretical exercise than a practical idea. It was basically me realizing that I could implement prototypes in a language that doesn't have prototypes, by using a function.

I then realized that since Arc is so malleable, I might even be able to extend the built-in eval to treat my special prototype functions as if they were hash tables. That, unfortunately, didn't work out, but I suspect I can do it with Anarki.

-----