@JorgWMittag's answer gives a very good theoretical grounding on a very important theoretical concept: an object can be simulated using a closure that selects behaviour based on a parameter. Unfortunately, this approach has a few problems:
- You need to write code to interpret the individual messages received by an object, which is something that you'd usually want your language to do for you
- In statically typed languages you'll have problems with different methods that handle different argument types.
One possible solution for the above two problems is to encode your protocol using an algebraic data type. For example, in Haskell, Jorg's code might look something like this:
data MyObjectProtocol = MyObjectProtocol_Add Int |
MyObjectProtocol_Sub Int |
MyObjectProtocol_ToString
type MyObject = MyObjectProtocol -> Either MyObject String
makeNewObject :: Int -> MyObject
makeNewObject state = handleRequests
where
handleRequests (MyObjectProtocol_Add amount) = Left $ makeNewObject $ state + amount
handleRequests (MyObjectProtocol_Sub amount) = Left $ makeNewObject $ state - amount
handleRequests (MyObjectProtocol_ToString) = Right $ "I am an object with value: " ++ show state
This still leaves a couple of problems:
- The various return types of the functions must be encoded in another type, which you'll have to pattern match on when you use the object, which is a long way from convenient.
- The way I've done this can't handle a method that both mutates the object and returns a value in an easy way. Probably the best way of solving this is to turn the object into a monad, but that makes the implementation quite complicated.
It would be better, therefore, to use multiple functions to encode the methods of the object and implement something a little more like virtual dispatch as used by most OO languages today, i.e. by storing a table of virtual method pointers (aka functions) in the object. You can then have a set of standard functions that fetch the virtual method from the object and execute it. This might look something like this:
data MyObject = MyObject (Int -> MyObject)
(Int -> MyObject)
(String)
myObject_add :: MyObject -> Int -> MyObject
myObject_add (MyObject f _ _) amount = f amount
myObject_sub :: MyObject -> Int -> MyObject
myObject_sub (MyObject _ f _) amount = f amount
myObject_toString :: MyObject -> String
myObject_toString (MyObject _ _ f) = f
makeNewObject :: Int -> MyObject
makeNewObject state = MyObject add sub toString where
add amount = makeNewObject $ state + amount
sub amount = makeNewObject $ state - amount
toString = "I am an object with value: " ++ show state
The result is a little more boilerplate in the definition of the object type, but makes it much easier to use the object. And if you have different implementations of the object type, the amount of boilerplate for each additional implementation is lower, so this is definitely a better way of working for interfaces that might have many implementations.