Objects Just Got Weirder

How Life Was

Brat has always had a kind of inheritance. You can always call new on an object to get a new object with the first object as its parent:

my_object = object.new

my_object.something = { "really something!" }

another_object = my_object.new

p another_object.something

In the example above, another_object has the something method defined on my_object.

Init

A couple years ago, you could define an init method on an object. This method would be called after a new child was created.

my_object = object.new

my_object.init = {
  my.something = "Jello!"
}

child_object = my_object.new

p child_object.something

In this case, any child of my_object will be initialized with a field called something.

The Problem

Let’s say you wanted to set up an object that works more like a ‘class’. That is, it has some methods useful for itself, and some only useful for objects created as its children. Here is an example:

person = object.new

person.init = { name |
  my.name = name
}

person.greet! = {
  p "Hello, #{my.name}!"
}

bob = person.new "Bob"

bob.greet!

Okay, so here we have a person ‘class’. When creating a new person, we provide a name. Then we can use greet! to greet the new person.

That’s all great, but there is a problem. person also has the greet! method, but no name! Calling person.greet! would produce an error like:

    Method error: object[greet!, parent] has no method called 'name'.

One way to get around this is to define name for person, which might make sense in some contexts, but can be completely nonsensical in others.

Another way to deal with this issue is to have two object: one is person, and the other is something like person_instance. Then, when calling new, we return a person_instance instead of a person. But that sort of confuses what person.new is supposed to do, plus it’s ugly. (However, this is basically how all the object written in Lua worked, especially in core.lua.)

Yet another way would be to just add the child methods in the init method:

person = object.new

person.init = { name |
  my.name = name

  my.greet! = {
    p "Hello, #{my.name}!"
  }
}

bob = person.new "Bob"

bob.greet!

This works, and sometimes it may be the right thing to do, but it is not really ‘inheritance’ anymore, because each child has its own version of greet!. If it were real inheritance, one should be able to change the greet! method on a parent and have that change propagate to the children.

The New Way

So, now there is a new way, much better than the old way.

Each object contains another object, called its prototype. When you create a new child object, the prototype of the parent will be added to the method lookup for the child. This way, you can add methods to the prototype which only the child will be able to call.

Here is the person example using the new way:

person = object.new

person.init = { name |
  my.name = name
}

person.prototype.greet! = {
  p "Hello, #{my.name}!"
}

bob = person.new "Bob"

bob.greet!

Now there is no person.greet! method to worry about, but at the same time changing the person.prototype.greet! method will change that method for all children objects.

Many Ways

Of course, why make things simple when you can make them confusing?

I have added not one, but three ways to set up your prototypes. The first way was shown above: person.prototype will return the prototype object, and you can add methods to it.

The second way is to pass a hash to person.prototype:

person.prototype greet!: {
  p "Hello, #{my.name}!"
}

The third way is to pass a function to person.prototype which will be called in the context of the prototype:

person.prototype {
  my.greet! = { p "Hello, #{my.name}!" }
}

Redefining

What if you call person.prototype multiple times? The effects are additive. If you add a method which has already been added, then it will replace the previous value.

Moving Up

This is all fine and good, but what if I have an object, and I want to add something to the prototype of its parent, so all the children get a new method? No problem!

person = object.new

person.init = { name |
  my.name = name
}

person.prototype.greet! = {
  p "Hello, #{my.name}!"
}

bob = person.new "Bob"

bob.parent.prototype.something_new = { "Something awesomely new, #{my.name}!" }

jenny = person.new "Jenny"

p jenny.something_new

p bob.something_new

Now all children of person will have the new method.

Calling Up

Okay, fine. But what if I want to call a method of a parent object, but in the context of the child? (Essentially, the super keyword in many languages.) Ah. You have caught me in an awkward spot. Because ‘methods’ in Brat are really just functions attached to objects, methods don’t even know their own names (they might have many), so what does it mean to say, ‘execute this method, but not this method, the one defined with the same name but on my parent object…’? Doesn’t mean much to me.

There is a way to deal with this, sort of. It just ain’t very purty:

person = object.new

person.init = { name |
  my.name = name
}

person.prototype.greet! = {
  p "Hello, #{my.name}!"
}

bob = person.new "Bob"

bob.greet! = {
  with_this bob.parent.prototype->greet!
  p "What a wonderful day this is!"
}

bob.greet!

Here, the bob object adds its own greet! method. However, it still wants to call the original one, then add its own message afterward.

So what it does is call the method on the prototype of the parent, but with bob as the object of the method.

Side note: In this case, we used bob.parent to get the parent. This works, and it is nice and specific. One could also use my.parent, but then things get weird should you use an object created with bob.new. In that case, bob is the parent, and there is no bob.parent.prototype method.

Wrapping Up

This prototype-thing solves an annoying issue for me, and I hope it works for you, too.

In the early days of Brat, objects like string would actually carry all string methods, which made little sense. string.reverse needs an actual string to work! This was solved with the string_instance idea, but that only really worked for code written directly in Lua. Straight Brat code looked really awkward when trying to explicitly split it up like that. Also, if you were expected blah.new to return an object with blah as the parent, but instead got an object with blah_instance as the parent…well, it just didn’t make sense.

comments by Disqus
Fork me on GitHub