“Binding” refers to the act of resolving a method name to a piece of invocable code. Usually, the function call can be resolved at compile time or at link time. An example of a language using static binding is C:
int foo(int x);
int main(int, char**) {
printf("%d\n", foo(40));
return 0;
}
int foo(int x) { return x + 2; }
Here, the call foo(40)
can be resolved by the compiler. This early allows certain optimizations such as inlining. The most important advantages are:
- we can do type checking
- we can do optimizations
On the other hand, some languages defer function resolution to the last possible moment. An example is Python, where we can redefine symbols on the fly:
def foo():
""""call the bar() function. We have no idea what bar is."""
return bar()
def bar():
return 42
print(foo()) # bar() is 42, so this prints "42"
# use reflection to overwrite the "bar" variable
locals()["bar"] = lambda: "Hello World"
print(foo()) # bar() was redefined to "Hello World", so it prints that
bar = 42
print(foo()) # throws TypeError: 'int' object is not callable
This is an example of late binding. While it makes rigorous type checking unreasonably (type checking can only be done at runtime), it is far more flexible and allows us to express concepts that cannot be expressed within the confines of static typing or early binding. For example, we can add new functions at runtime.
Method dispatch as commonly implemented in “static” OOP languages is somewhere in between these two extremes: A class declares the type of all supported operations up front, so these are statically known and can be typechecked. We can then build a simple lookup table (VTable) that points to the actual implementation. Each object contains a pointer to a vtable. The type system guarantees that any object we get will have a suitable vtable, but we have no idea at compile time what the value of this lookup table is. Therefore, objects can be used to pass functions around as data (half the reason why OOP and function programming are equivalent). Vtables can be easily implemented in any language that supports function pointers, such as C.
#define METHOD_CALL(object_ptr, name, ...) \
(object_ptr)->vtable->name((object_ptr), __VA_ARGS__)
typedef struct {
void (*sayHello)(const MyObject* this, const char* yourname);
} MyObject_VTable;
typedef struct {
const MyObject_VTable* vtable;
const char* name;
} MyObject;
static void MyObject_sayHello_normal(const MyObject* this, const char* yourname) {
printf("Hello %s, I'm %s!\n", yourname, this->name);
}
static void MyObject_sayHello_alien(const MyObject* this, const char* yourname) {
printf("Greetings, %s, we are the %s!\n", yourname, this->name);
}
static MyObject_VTable MyObject_VTable_normal = {
.sayHello = MyObject_sayHello_normal,
};
static MyObject_VTable MyObject_VTable_alien = {
.sayHello = MyObject_sayHello_alien,
};
static void sayHelloToMeredith(const MyObject* greeter) {
// we have no idea what the VTable contents of my object are.
// However, we do know it has a sayHello method.
// This is dynamic dispatch right here!
METHOD_CALL(greeter, sayHello, "Meredith");
}
int main() {
// two objects with different vtables
MyObject frank = { .vtable = &MyObject_VTable_normal, .name = "Frank" };
MyObject zorg = { .vtable = &MyObject_VTable_alien, .name = "Zorg" };
sayHelloToMeredith(&frank); // prints "Hello Meredith, I'm Frank!"
sayHelloToMeredith(&zorg); // prints "Greetings, Meredith, we are the Zorg!"
}
This kind of method lookup is also known as “dynamic dispatch”, and somewhere in between of early binding and late binding. I consider dynamic method dispatch to be the central defining property of OOP programming, with anything else (e.g. encapsulation, subtyping, …) to be secondary. It enables us to introduce polymorphism into our code, and even to add new behaviour to a piece of code without having to recompile it! In the C example, anyone can add a new vtable and pass an object with that vtable to sayHelloToMeredith()
.
While this is late-ish binding, this is not the “extreme late binding” favoured by Kay. Instead of the conceptual model “method dispatch via function pointers”, he uses “method dispatch via message passing”. This is an important distinction because message passing is far more general. In this model, each object has an inbox where other objects can put messages. The receiving object can then try to interpret that message. The most well-known OOP system is the WWW. Here, messages are HTTP requests, and servers are objects.
For example, I can ask the programmers.stackexchange.se server GET /questions/301919/
. Compare this to the notation programmers.get("/questions/301919/")
. The server can refuse this request or send me back an error, or it can serve me your question.
The power of message passing is that it scales very well: no data is shared (only transferred), everything can happen asynchronously, and objects can interpret messages however they like. This makes a message passing OOP system easily extendable. I can send messages that not everyone may understand, and either get back my expected result or an error. The object need not declare up front which messages it will respond to.
This puts the responsibility of maintaining correctness onto the receiver of a message, a thought also known as encapsulation. E.g. I can't read a file from an HTTP server without asking for it via a HTTP message. This allows the HTTP server to refuse my request, e.g. if I lack permissions. In smaller scale OOP, this means that I don't have read-write access to an object's internal state, but must go through public methods. A HTTP server doesn't have to serve me a file, either. It could be dynamically generated content from a DB. In real OOP, the mechanism of how an object responds to messages can be switched out, without a user noticing. This is stronger than “reflection”, but usually a full meta-object protocol. My C example above cannot change the dispatch mechanism at runtime.
The ability to change the dispatch mechanism implies late binding, since all messages are routed through user-defineable code. And this is extremely powerful: given a meta-object protocol, I can add features such as classes, prototypes, inheritance, abstract classes, interfaces, traits, multiple inheritance, multi-dispatch, aspect-oriented programming, reflection, remote method invocation, proxy objects etc. to a language that does not start out with these features. This power to evolve is completely absent from more static languages such as C#, Java, or C++.