Wednesday, September 3, 2008

Vim pr0n: Implementing prototype based objects

When vim 7 came along with support for lists (arrays) and dictionaries (hashes), nerds were delirious with joy. On the day it was released, geeks everywhere ripped off their pants and cartwheeled up and down their office aisles, their junk flapping freely in the air con. It was a great day to be alive.

It was also a great day to be blind.

One of the cool things about having dictionaries is that we can now implement prototype based OO. I've been playing around with this in vim script for a while now. If you are interested in doing any sort of OO programming with vim script then read this raving.

"Prototype Based OO" wtf yo?!


There are two types of object oriented programming: class based OO and prototype based OO.

In class based OO (e.g. java, c++, ruby etc) you write the blueprints for your objects then use those blueprints to create working object instances.

In prototype based OO (e.g. javascript, lua), you create a fully functional working object (i.e. a "prototype") and then clone that object. So the prototype object effectively serves as the class, while the clones of that prototype serve as the instances.

Getting started — methods, properties and constructors


In vim script the prototype object is defined as a dictionary, where the dictionary keys map to values (properties) and function references (methods).

Check this example out:


 1 "start the prototype
 2 let AK47 = {}
 3
 4 "the constructor
 5 function! AK47.New(ammo)
 6     let newAK47 = copy(self)
 7     let newAK47.ammo = a:ammo
 8     return newAK47
 9 endfunction
10
11 "an instance method
12 function! AK47.fire()
13     if self.ammo > 0
14         echo "BANG!"
15         let self.ammo -= 1
16     else
17         echo "click"
18     endif
19 endfunction
20
21 "at runtime we can do this:
22 let a = AK47.New(2)
23 echo a.ammo   " => 2
24 call a.fire() " => BANG!
25 call a.fire() " => BANG!
26 call a.fire() " => click


In this example, our prototype starts as an empty dictionary.

The first thing we add to the prototype is a constructor. There are a few ways you could do this, but I like to do it with a method called New(). It clones the prototype object (i.e. self), assigns the ammo instance property and returns the new object.

Next we add an instance method called fire() which "shoots" a bullet if there's any ammo left.

Notice that self is a reference to the current object. It is mandatory to use this reference when accessing members of the current object. In the above example, if self.ammo > 0 succeeds, whereas if ammo > 0 would result in an Undefined variable error.

Private methods and properties


There is no way to make methods or properties private. However, if you make up some conventions for yourself, then you can make the intent of your code clear (even though vim wont actually enforce it). I have the convention that any method or property starting with an underscore should be treated as private.


So, for example, I could make the ammo instance property "private" like this:


 1 "start the prototype
 2 let AK47 = {}
 3
 4 "the constructor
 5 function! AK47.New(ammo)
 6     let newAK47 = copy(self)
 7     let newAK47._ammo = a:ammo
 8     return newAK47
 9 endfunction
10
11 "an instance method
12 function! AK47.fire()
13     if self._ammo > 0
14         echo "BANG!"
15         let self._ammo -= 1
16     else
17         echo "click"
18     endif
19 endfunction


Class methods and class properties


Officially in vim script, there are no such things as class methods or class variables, but you can still implement them.

If a method doesn't access any instance variables or instance methods then, practically speaking, it's a class method. The New() methods above are examples.

If a variable is defined and accessed on the prototype object, then its a class variable.

I have some conventions I've been using:

  • I like to start all class members with a capital letter and all instance members with a lower case letter.

  • If I'm calling a class method from inside an instance method then I like to use the prototype name as the target object, i.e. TheClass.TheClassMethod() rather than self.TheClassMethod(). Similarly for class variables.


Inheritance


Prototyping with Vim script is an example of "Pure prototyping", or "Concatenative prototyping". Each object stands alone and has no links to its parent prototype. If you want to create a subclass, you must clone the parent prototype then add the new features to the clone. You can see why it's called called "Concatenative", since all the prototypes are joined together as you go down the inheritance tree.

This is in contrast to, say, javascript where, instead of cloning the parent, every object has a magic prototype property which points to the parent. The interpreter then uses this link for method dispatching, i.e. it searches back up the inheritance tree for the method definition.

Anyway, let's look at an example:


 1 "clone the AK47 prototype
 2 let AK47GL = copy(AK47)
 3
 4 "override the old constructor
 5 function! AK47GL.New(ammo, grenades)
 6     let newAK47GL = copy(self)
 7     let newAK47GL.ammo = a:ammo
 8     let newAK47GL.grenades = a:grenades
 9     return newAK47GL
10 endfunction
11
12 "define a new instance method
13 function! AK47GL.fireGL()
14     if self.grenades > 0
15         echo "OMG BOOOOOM!"
16         let self.grenades -= 1
17     else
18         echo "click"
19     endif
20 endfunction
21
22 "at runtime we can do this:
23 let a = AK47GL.New(2,1)
24 echo a.ammo     " => 2
25 echo a.grenades " => 1
26 call a.fire()   " => BANG!
27 call a.fire()   " => BANG!
28 call a.fire()   " => click
29 call a.fireGL() " => OMG BOOOOOM!
30 call a.fireGL() " => click


Here we define a new type of AK47 called AK47GL (an AK with an under-slung grenade launcher).

First we clone the AK47 prototype.

Then we replace the New method with one that accepts a grenade ammo counter. This is how we override methods. If we were really hardcore, we could rename the old New() method to something like _AK47_New() so that it will still be available to us, but I haven't bothered here.

Lastly, we define a new method called fireGL().

One thing to note about inheritance is that the subclass must be defined after the superclass in the code. Otherwise the initial copy() will fail.

Final ranting


We've seen that, using dictionaries we can create prototype objects with methods and properties. We can implement class methods and class variables. We can't implement private or protected members, but we can at least indicate our intentions with naming conventions. We've also seen how to implement inheritance.

I realise that there's a lot of stuff that I've left out, and if you need to know something I haven't covered here then the best source of information is other prototyping languages. Take a look at how people do it with javascript (the prototype library could be useful) or lua. Also, there's a whole list of prototype based OO languages here that you can steal ideas from.

4 comments:

  1. Vi sux u douchebag;

    ReplyDelete
  2. Why not do:

    let AK47GL = AK47.New(0)

    instead of using copy?

    ReplyDelete
  3. Araxia: if you did that it would have the effect of calling the parent constructor in addition to the child constructor; he wants to override it.  I don't really know why though. C++ doesn't call parent constructors by default either, so he's in mixed company.

    Also I wanted to point out that your AK47 is actually a cowboy pistol. Or I guess maybe a shotgun, since I don't think you could mount a grenade launcher on a cowboy pistol.

    Thanks for the overview.

    ReplyDelete