tychoish, a wiki

tychoish/cyborg/ objecctive-programming


Alaric Snell-Pym alaric@snell-pym.org.uk wrote:

You're kind of there, but sound a bit confused. Let me try and explain.

For a start, "object orientation" is a loaded term. If you try and formally define it, you come up with one idea of what OO is. If you play with most "modern OO languages" (Java, Python, C++, etc) you come up with another idea of what OO is. If you play with Lisp you get yet another idea of what OO is (generic functions).

So let's start with the Lisp model, as it's simpler.

Say you have a program that models some complex data model. Perhaps some tool for modelling the structure of businesses, which has things like "employees", "jobs" (as in, job functions of employees, such as 'writing software'), "tasks" (as in, specific things to do like 'post the accounts for the 2010 financial year to the accountant'); a task might be associated with a job (as it's part of that function), with several people (who said it should be done, who's doing it, who they should report to when it's done, etc); employees have jobs, departments have employees and department-wide jobs of their own, and so on. Oh, and employees are assigned bits of equipment, which are located at places in buildings, and those buildings have associated jobs such as "maintain the infrastructure in building X", and therefore people assigned to those jobs, and so on and on and on.

In other words, imagine a complex set of things, with relationships between them. If writing this in Lisp, we might create a "class" for each type of thing (job, employee, etc). What is a class? Things like "integer" and "pair" are already classes. A class really just names a type, and gives names to its parts: a cons cell has a car and a cdr, for example. An employee has a name, an address, salary details, and a start date; this are referred to as "slots". For each "slot" within a class, we need a function to extract the value of that slot, just as the function "car" extracts the car of a cons cell.

We might define a class like so:

(defclass employee ()

((name :accessor employee-name :initarg :name)

(address :accessor employee-address :initarg :address)

(start-date :accessor employee-start-date :initarg :start-date)))

That defines a class called "employee"; it has three slots (name, address, start-date), and defines three "accessor functions" to get the name, address or start date of an employee (called employee-name, employee-address and employee-start-date), and declares that when creating an employee one can specify the initial values of the slots with arguments called :name, :address and :start-date.

That defines a class. It defines what an employee is like. It does not create any actual employees - those are the objects. But having defined the class, we can now write:

(define me (make-instance 'employee :name "Alaric" :address "123 Any Street, Sometown" :start-date "2001-01-14"))

(employee-name me)

=> "Alaric"

(employee-address me)

=> "123 Any Street, Sometown"

That's all there is to creating an using a class in Common Lisp. A class is just a user-defined type, really, with its own internal structure. There's no mechanism for the program to be reading in data and figuring out how to turn it into objects, as you imply in your blog post - somewhere, the code has to decide to call "make-instance" for a particular class name (in this case, probably due to somebody in HR pressing the "Add Employee" button in your snazzy GUI). And having created an employee object from the employee class, your app has to decide where to put that employee object (perhaps in some kind of list of employees somewhere), or else it'll just get taken away by the garbage collector again.

Objects of user-defined classes are no different to the objects you're familiar with - integers, strings, cons cells, etc.

But that's not all there is to OO. For a start, there's inheritance. Say your application deals with employees AND customers AND shareholders. All of these things have some things in common (they're people, which have names and addresses). But employees also have a start date, customers also have an order history, shareholders also have a stock holding, and so on. Rather than creating separate classes for each that all share slots such as "name", we can create a class of people and then inherit that class into classes for employees and so on, like this:

(defclass person ()

((name :accessor person-name :initarg :name)

(address :accessor person-address :initarg :address)))

(defclass employee (person)

((start-date :accessor employee-start-date :initarg :start-date)))

(defclass customer (person)

((order-history :accessor customer-order-history :initarg :order-history)))

(defclass shareholder (person)

((shares :accessor shareholder-shares :initarg :shares)))

Having done that, you will now be able to create customers, employees, etc with make-instance, but you'll be able to use the person-name accessor function to extract the name of any of these kinds of objects, reglardless of what class they actually are. If you are writing a function to write a letter to a person, that prints out the letter and prints an envelope with the name and address on, then this function can be used to send letters to shareholders, employees, or customers. While a function that calculates dividends, and uses the shareholder-shares function, will of course only work on shareholders, not customers (although it can then use the generic send-letter function to write a letter to the shareholder telling them what their dividend payment is going to be!).

This is good, as it makes our code simpler; we can reuse the letter-sending functionality between different types of people.

But suppose we want to use a different letter-writing system for shareholders. Rather than using the cheap and nasty dot-matrix mass-envelope-labelling machine in the basement, we want shareholder letters to be printed on the nice laser with nice paper and put in a nice expensive-looking envelope?

Well, we could make our print-letter function work out if the person is a shareholder (there's a function for asking what the class of something is) and have a big (if ...) to decide between the two printing mechanisms.

But that's ugly.

Instead, we could use a generic function. If we declare a "print-letter" generic function, we can then provide two implementations for it - one for any person, and one for shareholders. When you call the generic function, Lisp decides which implementation to use; for a shareholder, both are eligible, but as 'shareholder' is more specific than 'person' (because it inherits from 'person'), the shareholder one will win. If you call it with a customer, then as there is no explicit implementation for a customer, the generic 'person' implementation gets used. And if you call print-letter on, say, a cons cell, then as there is no eligible implementation at all, you get an error.

This is sort of the same thing as that big "if", with one major exception: each implementation of a generic function is defined independently. So the bit of your application that deals with printing letters can define the generic function and the general 'person' implementation of it, while a special module for dealing with shareholders (written by the shareholder relations team, and in a different part of the app entirely) can define the shareholder implementation. While programmers from any part of the organisation can call "print-letter". And if an extra bit of the software is designed one day to send letters to employees more cheaply by using the internal post system rather than putting them in envelopes at all, it can be written and just define a new implementation of "print-letter" for employees. Whereas using the "big if" approach, to add this new letter-printing system you'd need to talk to the programmers who maintain the "print-letter" function to get your employee case added. In effect, Lisp maintains that "big if" inside print-letter for you, assembling it dynamically from all the letter-printing implementations you provide it.

This is a benefit when writing large applications with large teams; in effect, Lisp assembles your program for you from the bits provided by different teams. Which is why OO is popular in the "large software systems" world, and sometimes less useful for one-man projects.

Java, C++, Python, etc. complicate matters by not having generic functions, and instead having methods that live inside the classes/objects like slots do. So the methods get inherited like slots, which means that you can write a generic print-letter method inside "person", then "override" it with another one inside "shareholder" to get specific behaviour when printing letters to shareholders. However, this means that you still need to bother the authors of the "shareholder" class to get your improved "print-letter" method added, despite the fact that the "shareholder" class is owned by the folks who design the data model while "print-letter" is the responsiblity of the letter-printing department. As usual, Lisp does it better ;-)