Objects Without Objects
Caution, dear reader: there's no point to this particular entry, I'm just rambling.
Then again, aren't I always?
I've been idly doodling a programming language and it led me to thinking about the nature of objects. Crazy how they do. Poking around in that thoughtspace, I was wondering how far you could get with just plain ol' functions, which is the ill-defined road we're going to wander down today.
This isn't so hard actually! Here's the deal: we'll call an "object" a function that takes a method name and some arguments. So, for example:
greeter = (method, name) ->
if method == "greet"
console.log("Hello, " + name)
else
throw new Error("Unknown method")
greeter("greet", "lovely reader")
This is OO in action: we send a dude a message and it does something. This scheme is all we need; it handily gives us polymorphism:
excitedGreeter = (method, name) ->
if method == "greet"
console.log("Hello, " + name + "!")
else
throw new Error("Unknown method")
greeters = [greeter, excitedGreeter]
for someGreeter in greeters
someGreeter("greet", "lovely reader")
We can even do mutable state, right:
createCounter = ->
count = 0
return (method) ->
switch method
when "count"
count += 1
when "get"
count
else
throw new Error("Unknown method")
counter = createCounter()
counter("count")
counter("count")
console.log(counter("get"))
# => 2
Cool! But we're already starting to see a lot of boilerplate. I bet we can cut that down some.
Attributed To Attributes
Let's take the JavaScript view of objects. What is an object? A miserable little pile of attributes. We can do that, no prob. But first we'll need the duct tape of programming: dictionaries. If we could use objects, well, we'd use 'em here, but that's off the table, so we'll define our own:
emptyDictionary = null
get = (dictionary, key) ->
if dictionary?
return dictionary(key)
else
return null
set = (dictionary, key, value) ->
(someKey) ->
if someKey == key
return value
else
get(dictionary, someKey)
aDictionary = set(emptyDictionary, "key", "value")
console.log(get(aDictionary, "key"))
# => value
Perfect!
(Try not to think about how badly this thing will blow up on deep searches for keys)
Okay, let's get to it. An object will basically just close over a dictionary of attributes. It'll handle messages by looking up the corresponding function and calling it on itself with the arguments:
newObject = ->
attributes = emptyDictionary
setAttribute = (self, name, value) ->
attributes = set(attributes, name, value)
attributes = set attributes, "setAttribute", setAttribute
self = (methodName, args...) ->
method = get(attributes, methodName)
if method?
method(self, args...)
else
throw new Error("Unknown method")
greeter = newObject()
greeter "setAttribute", "greet", (self, name) ->
console.log("Hello, " + name)
greeter("greet", "lovely reader")
Straightforward enough, right? An object'll come with one baked-in method, setAttribute
, and it can build everything from that. Sure, we're skipping over some error handling, but shwatevs. That'll be a common theme.
What next? Hm. How about prototypical inheritance? We'll take JavaScript as our model again. When an object looks up an attribute and it doesn't find it on itself, it should look it up in its parent. But how do we do that?
Inheriting Inheritance
Well, first, let's get a little more flexibility in our objects. Right now, getting attributes is hardcoded as a direct dictionary lookup, but what if it was just another method?
newObject = ->
attributes = emptyDictionary
getAttribute = (self, name) ->
get(attributes, name)
attributes = set attributes, "getAttribute", getAttribute
setAttribute = (self, name, value) ->
attributes = set(attributes, name, value)
attributes = set attributes, "setAttribute", setAttribute
self = (methodName, args...) ->
getAttribute = get(attributes, "getAttribute")
method = getAttribute(self, methodName)
if method?
method(self, args...)
else
throw new Error("Unknown method")
Okay, there's still a bootstrapping lookup in the attributes to get getAttribute
itself, but from there, the object can do what it wants.
How does this help us? Well, now we can redefine getAttributes
for inheritance:
object = newObject()
object "setAttribute", "extend", (parent) ->
child = newObject()
getOwnAttribute = child("getAttribute", "getAttribute")
child "setAttribute", "getAttribute", (self, name) ->
getOwnAttribute(self, name) or parent("getAttribute", name)
child "setAttribute", "getOwnAttribute", getOwnAttribute
return child
parent = object "extend"
parent "setAttribute", "parentMethod", (self) ->
"parentValue"
child = parent "extend"
console.log(child("parentMethod"))
# => parentValue
grandChild = child "extend"
console.log(grandChild("parentMethod"))
# => parentValue
Let's revisit our counters now:
baseCounter = object "extend"
baseCounter "setAttribute", "init", (self) ->
self("setAttribute", "_count", 0)
baseCounter "setAttribute", "count", (self) ->
self("setAttribute", "_count", self("getAttribute", "_count") + 1)
baseCounter "setAttribute", "get", (self) ->
self("getAttribute", "_count")
counter = baseCounter "extend"
counter "init"
counter "count"
console.log(counter("get"))
# => 1
Ack, whose keyboard had indigestion? Let's clean that up some.
Refining Defining
Mm, first off, defining new methods is super repetitive. We reduce it a bit by adding a callback to define new methods when extending:
object "setAttribute", "extend", (parent, onDefinition) ->
child = newObject()
# ...
if onDefinition?
defineAttribute = (name, value) ->
child("setAttribute", name, value)
onDefinition(defineAttribute)
return child
nicelyDefinedThing = object "extend", (def) ->
def "someMethod", (self) ->
console.log("nice!")
nicelyDefinedThing("someMethod")
# => nice!
Second, the explicit init
calls to set up the initial state. Let's make that happen automatically with a call to new
:
object "setAttribute", "new", (parent, args...) ->
child = parent "extend"
child "init", args...
return child
newableThing = object "extend", (def) ->
def "init", (self) ->
self("setAttribute", "inited", true)
newThing = newableThing "new"
console.log(newThing("getAttribute", "inited"))
# => true
Finally, state. All these getAttribute
and setAttribute
calls just to set state. Let's add some smarts to our objects. @var
will get a value for an instance variable var
and @var=
will set it.
We'll do this by intercepting the method lookup and returning a getter or setter based on its name if it starts with @
:
oldGet = object "getAttribute", "getAttribute"
object "setAttribute", "getAttribute", (self, name) ->
if name[0] == "@"
if name[name.length-1] == "="
name = name.substring(0, name.length - 1)
return (self, value) ->
vars = self("getOwnAttribute", "instanceVariables")
vars = set(vars, name, value)
self("setAttribute", "instanceVariables", vars)
else
return (self) ->
vars = self("getOwnAttribute", "instanceVariables")
return get(vars, name)
else
oldGet(self, name)
myStatefulThing = object "extend"
myStatefulThing("@state=", "some state")
console.log(myStatefulThing("@state"))
# => some state
Now our definition looks like this:
baseCounter = object "extend", (def) ->
def "init", (self) ->
self("@count=", 0)
def "count", (self) ->
self("@count=", self("@count") + 1)
def "get", (self) ->
self("@count")
counter1 = baseCounter "new"
counter1 "count"
console.log(counter1("get"))
# => 1
counter2 = baseCounter "new"
counter2 "count"
counter2 "count"
console.log(counter2("get"))
# => 2
That's not so bad, is it?
Okay, let's do one last thing: dispatching to the parent.
Proud Parents
Remember the excitedGreeter
? It's, uh, the greeter
but more different:
greeter = object "extend", (def) ->
def "greet", (self, name) ->
console.log("Hello, " + name)
excitedGreeter = object "extend", (def) ->
def "greet", (self, name) ->
console.log("Hello, " + name + "!")
We should be able to leverage the greeter
implementation somehow, right? Right now we can write:
greeter = object "extend", (def) ->
def "greet", (self, name) ->
console.log("Hello, " + name)
excitedGreeter = object "extend", (def) ->
def "greet", (self, name) ->
greeter("getAttribute", "greet")(self, name + "!")
But that's a little cumbersome.
Well, what if we defined parent
to call the parent object's implementation?
object "setAttribute", "extend", (parent, onDefinition) ->
child = newObject()
child "setAttribute", "parent", (self) ->
(methodName, args...) ->
parent("getAttribute", methodName)(self, args...)
# ...
return child
greeter = object "extend", (def) ->
def "greet", (self, name) ->
console.log("Hello, " + name)
excitedGreeter = greeter "extend", (def) ->
def "greet", (self, name) ->
self("parent")("greet", name + "!")
excitedGreeter("greet", "lovelier reader")
# => Hello, lovelier reader!
That seems to work, but what if we inherit again?
evenMoreExcitedGreeter = excitederGreeter "extend", (def) ->
def "greet", (self, name) ->
self("parent")("greet", name + "!!")
evenMoreExcitedGreeter("greet", "lovely reader")
# => RangeError: Maximum call stack size exceeded
Ah jeez, what even?
The problem is that the parent
lookup is too dynamic. evenMoreExcitedGreeter
calls excitedGreeter
's greet
, which calls evenMoreExcitedGreeter
's parent
's greet
, which is just excitedGreeter
's greet
again! Bwaugh!
What we want to do is call to the parent of the exact object for which those methods are defined. Let's extend our extend
mini-language with a way of doing that:
object "setAttribute", "extend", (parent, onDefinition) ->
child = newObject()
child "setAttribute", "parent", (self, name) ->
parent("getAttribute", name)
getFromParent = (name) ->
child("parent", name)
getOwnAttribute = child("getAttribute", "getAttribute")
child "setAttribute", "getOwnAttribute", getOwnAttribute
child "setAttribute", "getAttribute", (self, name) ->
getOwnAttribute(self, name) or getFromParent(name)
if onDefinition?
defineAttribute = (name, value) ->
child("setAttribute", name, value)
onDefinition(defineAttribute, getFromParent)
return child
Note the new getFromParent
function that looks up an attribute through the child
's parent
. We're passing it to onDefinition
and we modified the inheritance stuff to use it too. Now we can call to the right parent:
greeter = object "extend", (def) ->
def "greet", (self, name) ->
console.log("Hello, " + name)
excitedGreeter = greeter "extend", (def, parent) ->
def "greet", (self, name) ->
parent("greet")(self, name + "!")
excitedGreeter("greet", "lovelier reader")
# => Hello, lovelier reader!
evenMoreExcitedGreeter = excitedGreeter "extend", (def, parent) ->
def "greet", (self, name) ->
parent("greet")(self, name + "!!")
evenMoreExcitedGreeter("greet", "loveliest reader")
# => Hello, loveliest reader!!!
Groovy! One last little thing here though: parent
is just another method. That means we've got a hook into how we look up parent values. That makes it easy to, say, write mixins:
object "setAttribute", "mixin", (self, objects...) ->
getFromParent = self("getAttribute", "parent")
for object in objects
do (object) ->
oldGetFromParent = getFromParent
getFromParent = (self, name) ->
oldGetFromParent(self, name) or object("getAttribute", name)
self("setAttribute", "parent", getFromParent)
return self
animal = object "extend", (def) ->
def "describe", (self) ->
console.log("I " + self("move") + " and " + self("talk"))
walkMixin = object "extend", (def) ->
def "move", (self) -> "walk"
swimMixin = object "extend", (def) ->
def "move", (self) -> "swim"
neighMixin = object "extend", (def) ->
def "talk", (self) -> "neigh"
horse = animal("extend")("mixin", walkMixin, neighMixin)
horse("describe")
# => I walk and neigh
seahorse = animal("extend")("mixin", swimMixin, neighMixin)
seahorse("describe")
# => I swim and neigh
Phew! Okay! I think let's call it quits maybe!
So hey, what'd we do today? We discovered that you can build a pretty righteous object system using plain ol' functions. Starting with message dispatch, we built up state, prototypical inheritance, and mixins. That's pretty cool!
The Dang Lang
How does the programming language I mentioned waaay back at the start tie into this? Partly smoothing over some of the roughness of stuff like the extend
mini-language with built-in syntax and partly expanding on the method dispatch.
Remember when we added special handling for names starting with @
? That's a taste of doing more interesting dispatch than a straight dictionary lookup of the method name. Another path to explore would be dispatching on the arguments too. But oh, I've said too much. That's a subject for another blog.