Erlang as a Model Object Oriented Language
The more I program in Erlang, the more I am convinced it is a model language for Object Oriented Programming. If you replaced the word "process" with the word "object", you'll discover a world of pure message passing object orientation with strict encapsulation. The pattern matching, proper tail recursion, anonymous first class functions, and list comprehensions are all just window dressing.
Probably the easiest way to get thinking about it is:
Object allot init. == spawn(Object,init,).
Spawn allocates a new process. From the Object Oriented Programmer's perspective, spawn allocates a new object of the supplied type (Erlang module) and invokes the initialization routine. If you were writing this in Objective-C it would look like:
Object allot init ;
Because the spawn(M,F,A) form allows for passing a list of arguments to init function, it allows the initialization function to set initial state:
Counter allot init: 1
Would be expressed as:
Where Counter:init/1 would look something like:
This design, squirrels away internal state in the process dictionary which is only accessible to the process itself. From this point on, the only way to mutate the state of this object is to send it messages. From this point on we are going Pure Object Oriented (POO). If we have a main loop that can spawn new objects we get something like this:
Sender ! spawn(?Module,init,Args), main();
This allows any instance to create a new instance of the same type. More over, as we can program entirely by sending messages to existing objects once we make this method dispatch fully dynamic:
case erlang:get(Method) of
Function when is_function(Function,length(Args)) ->
_ -> does_not_understand(Method)
This sort of dispatch looks for a function of the correct arity in the process dictionary and dynamically invokes it if found. If not found or of wrong arity, the does_not_understand handler is invoked. Typically, a DNU will sasl log the missend, or invoke the debugger. This makes tracking down bugs in development easy and allows for altering production behavior to be consumer friendly.
With dynamic dispatch in play, we can easily define new behaviors at run time:
put(does, fun(Method,Function) when is_function(Function) ->
With this method attached we can say:
O ! self(), does, hi, fun() -> io:format("hello world!") end.
At which point we can send another message to the object O:
O ! self(), hi .
And the string "hello world!" will be printed to the console. Using this functionality, we can dynamically replace behaviors at run time. Say we have a delay of 50000 milliseconds that we want to reduce to 5000. We could initially define the behavior as:
O ! self(), does, delay, fun() -> 50000 end .
and then when we want to reduce the delay send:
O ! self(), does, delay, fun() -> 5000 end .
Rather than changing a hidden property and providing accessors for that property, we can merely supply a function with the desired response. This technique supports a more complex event model, wherein each invocation of a method may trigger a chain of events. For example, say we want a method to turn off a series of lights:
put(off, fun() -> O ! [ self(), off || O <- get(lights)() ] end ),
If we have lights be a method, it can be used to query the current lighting configuration. But we can also manipulate it to return different presets:
put(house, fun() -> put(lights, fun() -> 1,2,4,5,6,8,9 end) end),
put(stage,fun() -> put(lights, fun() -> 3,7 end) end),
put(all, fun() -> put(lights, fun() -> 1,2,3,4,5,6,7,8,9 end) end),
Each behavior may define, enable, or disable other behaviors. This fluidity of interface allows objects to adapt to their circumstances, providing only those capabilities relevant to the current state of the object. For example, consider a File object, initially it may only have one method:
File ! open, "file name" .
The process of opening the file will remove the ability to open the file (as opening again is either an error or no-op), and provide new methods:
File ! read, Bytes .
File ! write, Data .
File ! close .
If we read or write, we may not alter the topology of the Objects's interface, but in the case of close, it will revert back to accepting only open again:
File ! close ,
File ! open, "some_other.file" .
The sequence of mutations defines a state machine which limits the possibility of spurious interactions that lead to undefined behavior. If your objects ignore unexpected or erroneous messages, your systems can often survive an entire category of aggregation bugs that result from combining pre-existing modules in unexpected ways.
Let's pretend you are building a GUI and each widget in your GUI responds to a set of state transition events:
Widget ! show.
Widget ! hide.
Widget ! draw.
And due to requirement changes, the designer decides to implement two new state transitions:
Widget ! .
Widget ! .
In which each widget is to perform a translation or opacity transitions over some region or time. Now initially, these behaviors will not exist on any widgets, and in a normal programming model, with immutable class interfaces, it would be difficult to incrementally implement these methods on a one by one basis, and test in an interactive environment. In fact, one would need to add stubs or guards in inappropriate places to ensure that each object could safely receive these messages, prior to any investigation. With mutable interfaces, however, we can add the transition methods one by one, and interactively test our changes. If at some later point, we get a requirement to swap out the behavior on an object or two, we can implement those state transitions on an ad hoc basis. And since this is Erlang, we can always do it live from a remote machine on the other side of the world!
By adopting a behavioral model of Objects, implementing a Self style slots object via process dictionaries, and limiting all communication between objects to strict message passing (no shared state), we can write Erlang that is a model for all Object Oriented Programming Languages. More over, the core of this requires no programming constructs outside of the core language and preserves all of the benefits of the functional approach. Avoidance of such dubious practices as inheritance, in favor of a more ad hoc construction of objects, promotes flexibility and adaptability over a misguided goal of completeness. I would argue any Class system is predicated on a faulty assumption that objects can be made complete or finalized, born out of a mathematical hubris rather than empirical analysis. But if one looks at the 30+ year history of Smalltalk, there is ample evidence to the contrary.