Pigeon Core - Objects

Object Philosophy

Object oriented programmers like to think of functions as being merely a special kind of object.

Functional programmers like to think of objects as being merely functions that have closed over some instance data, and take a message selector as their first parameter.

Pigeon likes to do both, at the same time.

An object is indeed a function that takes a message selector as the first parameter. Meanwhile, a function is an object that implements the call message. Turtles all the way down...

Object Messaging

When you write:

(function a b c)

this means to call the specified function, passing it a, b, and c as parameters.

When you write:

(message: object a b c)

this means to pass the specified message to the specified object, with a, b, and c as parameters.

If you are an object oriented programmer, you might like to think of the first version as a shortcut for the second, where the lack of an explicit message implies a prefix of call:. That is in fact pretty much how the virtual machine handles it, if you ignore optimisation complexities.

If you are a functional programmer, you might like to think of the second version as a call to the function message:, the behaviour of which is to do a dynamic method lookup in the specified object, then chain to the resulting function. This is also somewhat true, except for the fact that message: isn't a real function. It is scoped purely within the specified object, rather than lexically for a normal function, so you cannot globally redefine it for all objects. You can rewrite it using a macro definition, though, because macros expand statically at compile time rather than dynamically according to runtime scoping rules..

The message: syntax is actually equivalent to the string constant 'message', except that you can't override 'message' as a macro, because it is a constant and macros only apply to symbols. You can write things like:

('message' object a b c)

or even:

(def (send_message
  (lambda (msg object (args *))
    (msg object (* args)))))

(send_message 'message' object a b c)

In other words, the effect of calling a string as a function is to send itself as a message to the first parameter object. You could say that strings are functions which send messages, or perhaps that they are objects which dispatch new messages in response to receiving a call message, or maybe even that they are messages themselves. Aren't recursive definitions great?

Object Systems

When it comes to actually creating new objects, there are at least two possibilities. At the lowest level, if you send the objectify message to a function object, you get back a new object which uses that function to handle incoming messages. It is up to you to implement whatever dispatch behaviour or inheritance system you might want. It can be a lot of work to provide all the standard object functionality, but the flexibility is there if you need it. For instance this is a (rather useless) object which simply echoes whatever messages it is sent:

(def (object
  (objectify:
    (lambda (msg (args *) (keys *))
      (print msg)))))

(type: object)
(equals: object nil)
(understands: object 'turtles')
(turtles: object 'they\'re everywhere!')

More commonly, you will use the standard class type as a factory for creating new types. This provides a prewritten message dispatch routine, which not only saves coding effort, but is likely to be faster because it is implemented directly in C++. You do lose some flexibility compared to rolling your own, though.

The cool thing is that because all message sends ultimately boil down to calling a function, any number of different object systems can happily coexist, sending each other messages like turtles racing around a moebius strip. Pigeon objects encapsulate not only their own implementation, but the entire underlying object system as well!