I have an idea I’ve been holding back for a while because I think it is wrong. It’s just too general to be true, and the argument that I use for it is, well, a bit abstract, but I think there is something there. Here it goes.
Object-orientation is better for the higher levels of a system, and functional programming is better for the lower levels.
Interesting idea, but how do we get there?
Well, for me it comes back to the basics of the approaches.
There are many different flavors of functional programming - many different definitions, but at their core is one central idea: we can make programming better by reducing side effects. When you don’t have side effects, you can more easily reason about your code. You have referential transparency - the ability to paste an expression from one place in your program to another, knowing that it, given the same inputs, will produce exactly the same outputs without causing nasty side effects someplace else. The technical name for this is ‘purity’.
Purity gives us more than just a bit more ease in understanding, it enables lazy evaluation. In case you haven’t run into this before, in some functional programming languages there isn’t any mention of ‘calling a function.’ Instead, you ‘apply the function.’ This is more than just different nomenclature. The expression [1..10] evaluates as 1,2,3,4,5,6,7,8,9,10 in Haskell. If you apply the function take to that sequence with an argument of 5 (take 5 [1..10]), you get 1,2,3,4,5, the first 5 elements of the sequence.
What do you think happens when you evaluate [1..] in Haskell? Well, that gives you an infinite sequence of ints starting from 1. At an interactive prompt, you’ll have to hit Ctrl^C at some point to stop the printing. Okay, so what about this expression?
take 5 [1..]
You might think that it will run forever also. After all, [1..] has to be evaluated before it is passed to take, and it never stops. With lazy evaluation, though, this doesn’t happen. You aren’t calling take, you are forming an expression through function application. When you evaluate that entire expression, the runtime does only as much evaluation of the sub-expressions as it needs to do return the first result: 1, and then it evaluates for the second result, and all the way up to the limit of 5.
Lazy evaluation can be very powerful, but here is the key: it is completely enabled by purity. If you want to see this, imagine a large functional expression with a side-effect hidden deep inside it. When exactly does it happen? There really is no telling. It depends on the context the expression is evaluated in. Ideally, you shouldn’t have to care.
Object-Orientation has some parallel affordances. Again, there are a wide variety of different definitions of OO, but I like to go back to Alan Kay’s original conception. After all, he invented the term.
Alan Kay saw objects as a way of creating complex systems that is pretty much in line with how nature handles complexity in biology. In an organism, there are many cells, and the cells communicate by passing chemical messages between them. It isn’t just coincidence that Smalltalk uses the notion of a ‘message send’ rather than function call. The nice thing about object structure is that it de-emphasizes the players and maximizes the play. As Kay implied, the messages are more important than the objects. In a biological system, this goes as far as systemic redundancy. You can’t bring down the whole organism by killing a single cell. The closest we’ve gotten to that in software is Erlang’s process model which has been likened to an ideal object system.
In the early 2000s, Dave Thomas and Andy Hunt wrote about a piece of design guidance that they called ‘Tell, Don’t Ask.’ The idea was that objects are really best when you tell them to do something for you rather than asking them for their data and doing it yourself. This makes perfect sense from the point of view of encapsulation. It also makes it easier on the user of the object. When you get data back, you have to understand it and operate on it. This isn’t quite as simple as as telling an object to do something with its own data.
In biology, as I mentioned before, we have chemical messages between cells. But, they are different from typical OO in one very important respect - communication between cells is asynchronous. A cell doesn’t block all of its internal activity until it receives a response. Quite often, object systems do this. We send a message to another object, and we wait for a response. A response consists of a return value (data), and when we receive it, we are in the hot seat. We have to do something with it or choose to ignore it - the flow of control is back in our hands. It’s rather easy to end up violating 'Tell, Don't Ask' when you do synchronous calls. Synchronous calls often return values, and after all, a return value is the result of an implicit ‘ask’.
If we look at OO as being cell-like, it seems that much of our technology has it wrong. We can have classes and objects in a programming language, but as long as we make synchronous calls, the pieces aren’t as independent as they could be. Are there technologies that are more OO than we call OO? Yes. Messaging systems in IT architectures are all about gaining this level of independence.
So, we’ve looked at object oriented design and functional programming. I think there is a real parallel between them.
In OO, it is better to tell. When you tell, you maximize decoupling between entities. If you want to prevent re-coupling, you make your message sends asynchronous. This enabled by the tell model. In functional programming, it is better to ask. In fact, in pure functional programming, there is no other way to do things. A function which returns nothing is pointless unless it has a side effect, and we want to avoid those. Functional purity enables laziness in much the same way that OO enables asynchrony.
Now, if we accept these premises, what makes the most sense when organizing a system? We could have a functional layer on top executes message sends internally when expressions within it are evaluated, but that could be problematic for systems understanding. Moreover, it could yield side-effects outside the functional layer and violate purity.
What about the other direction? What if we put the object layer on top and allowed the objects to use functional pieces below? There isn’t any problem with that, really. Side-effect free functions are ideal for internal machinery. Object-orientation is great for the upper layer, where decoupling and information hiding are paramount.
So, that is the argument. And I know that it not always “true.” Languages like Scala allow programmers to freely mix objects and functions at any level of abstraction. You can clearly have functions which select and filter objects. Microsoft’s LINQ technology is all about having a functional layer on top of objects. Despite this, I think that my argument has a grain of truth. OO as originally conceived is very different from OO in practice. Or, to put it another way, we have OO now, but it is really more at the service and messaging levels in modern software architecture. At that level of abstraction, it seems to be true - it's better to tell above and ask below.