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.
So what if we apply SOA concepts at a lower level? I've been thinking along the lines of having only async communication between objects by implementing an in-process message bus and having objects act like mini SOA services.
I'd assumed I'd need the concept of events (tell) and async request (ask), but now I'm not so sure. It comes down to your distinction between functional and OO approaches, both of which seem reasonable for services. An OO service can be as stateful as it likes, because we can only tell it about things, not ask anything of it; an observationally immutable, idempotent service is also appealing for obvious reasons.
I need to get that blog post written...thanks for the thought provoking article.
Posted by: Akash Chopra | March 20, 2012 at 01:55 PM
It seems to me you're on to something, and I agree with Akash that it's related to SOA -- but the other way round: High level services (which I like to call systems) encapsulate both state and logic, and they interact with the outside world through the messages they exchange with it. Within each service's implementation, there'll be a logic layer and some persistency, and FP is great strategy for modularizing the logic. One convincing detail that supports this is that re-use never happened at the object level; it's always a larger, cohesive unit that gets re-used in a black-box kind of way. Maybe this can be explained by fine-grained re-use requiring freedom from side-effects.
Posted by: Stefan Tilkov | March 20, 2012 at 02:36 PM
One detail that may add some weight to your claim is the direction of the design process. FP is almost always a bottom-up affair; I don't think I've ever seen FP educational matter that emphasized top-down design over starting at the bottom and successive refinement. OOP, on the other hand, is usually taught with emphasis on top-down design, patterns and architecture.
Posted by: Daniel Lyons | March 20, 2012 at 03:03 PM
Steve Freeman and Nat Pryce wrote about similar topics called peers and internals. They prefer to build up internal object behavior using the functional style of programming while using message passing between objects. Their definition of internal doesn't necessarily limit them to the functional approach but a preference to it.
Regardless of the programming paradigm, you'll need to employ the Tell Don't Ask principle to centralize the decision making process. Disparate components cannot interact in a meaningful way unless they share the same context which is why context indepedence is critical to the composability and testability of a system.
Posted by: jmis | March 20, 2012 at 05:24 PM
"Purity ... enables lazy evaulation"
I think you have this backwards. Laziness enforces purity (p24):
http://research.microsoft.com/en-us/um/people/simonpj/papers/haskell-retrospective/haskellretrospective.pdf
Also, have you read Marks and Moseley "Out of the tar pit"?
http://web.mac.com/ben_moseley/frp/paper-v1_01.pdf
Posted by: Alistair Bayley | March 20, 2012 at 08:25 PM
This is largely how I view things as well. Also, I've been reading through "Growing Object Oriented Software, Guided By Incredibly Long Book Titles", and they say something similar.
Posted by: Avdi Grimm | March 20, 2012 at 11:35 PM
In "Growing Object-Oriented Software, Guided by Tests", the authors say the following:
"we find that we tend towards different programming styles at different levels in the code. Loosely speaking, we use the message-passing style we’ve just described between objects, but we tend to use a more functional style within an object, building up behavior from methods and values that have no side effects.
Features without side effects mean that we can assemble our code from smaller components, minimizing the amount of risky shared state. Writing large-scale functional programs is a topic for a different book, but we find that a little immutability within the implementation of a class leads to much safer code and that, if we do a good job, the code reads well too."
Posted by: Philip_schwarz | March 21, 2012 at 01:52 AM
The juxtaposition of "don't ask tell" and functional programming is an interesting idea - I need to think it over - but I don't quite agree with the "functional programming at the low level" implication. Side-effect free libraries are useful - but it does not mean that they have to be purely functional internally - they just need to be purely functional from the outside. They can have state that is rebuilt with each invocation anew. I think that imperative programming is just easier then functional programming - but if you could enclose it inside modules and have the compiler checking that it is properly closed - then you could use imperative programming inside libraries that look pure from the outside. In a way this is the same as how functional languages work on an imperative Random Access Machines.
Posted by: zby | March 21, 2012 at 03:20 AM
I think I've tended to prefer something like this for a while. Part of this is prefering libraries to frameworks for dependencies as it reduces the overall complexity of the system IMO. In addition, the lowest libraries (like Joda-Time) are immutable/functional.
My suspicion is that one thing preventing more use of immutable structure at higher levels is their ease of use. Specifically, editing a single property of a deeply nested structure is a tricky prospect without language support.
Posted by: Stephen Colebourne | March 21, 2012 at 04:36 AM
Have been using this dual approach very successfully with JavaScript the last year or so.
Functional for actions doable in isolation.
OO to divide program into responsibilities/capabilities and handle path choices.
Think it works better than only using one way for the whole application. Both objects and functions turn out reasonably simple for there combined complexity. And there is no mixing of styles, each style in its own area.
Posted by: mncl | March 21, 2012 at 05:45 AM
Your proposal sounds very similar to what is said about OOP and FP in the "Object Oriented Programming" Lab included with the J programming language. In section 5 of that lab, it says:
"It is a mistake to think of OOP as an alternative to FP. All languages, at the low level, have FP, and at a higher level, have OOP."
I don't wholly agree with that claim, but I do like the way it points in the same direction as your idea: Large-scale modules benefit from OOP techniques, small-scale components benefit from FP techniques.
Putting this idea into practice, we must become willing and able to write very different programs within these two layers. We must also find a way to interface between, i.e. program across, these two layers.
Those difficulties are great enough that most projects don't do it. By and large, management does not recognize that this two-tier approach is an option, but if the option were clear to them managers might often decide not to try it. It simplifies things to have a single tier, even if that does mean experiencing the method-by-method mis-fit of applying OOP to small-scale programming tasks. Perhaps this helps us understand why C++, Java, and C# have their patterns of success.
Posted by: Tracy Harms | March 21, 2012 at 07:19 AM
This insight is also consonant with the advice given in Peter Van Roy's CTM. The book recommends keeping most of the logic in a purely functional core (actually they recommend a single-assignment dataflow style, but it's close enough) surrounded by a concurrent Actor-style layer.
I'm inclined to agree, but one thing I wouldn't know how to handle is the use of the Separated Interface pattern to trigger side-effects. For instance, it's common to have a domain model object call a notification dependency if some condition applies (say, the user exceeded his daily quota), this notification interface could have an implementation that sends an email to the user. Another common use of the separated interface pattern is to pass a repository interface to a domain object that may (or may not, depending on some logic) call it to obtain other objects from a database; the repository would be implemented as a sort of data access object on the persistence layer. If we restrict the core domain model to be purely functional, we could no longer apply this modeling technique; I guess an alternative would be to wrap logic that may or may not cause side effects in a monad, but I'm unsure about the gains.
Maybe the generic objection is how to handle IO side-effects when the decision to produce them is complex and they may be parametrizable.
Sorry for the ramble, perhaps someone has a more concrete idea.
Posted by: Rafael de F. Ferreira | March 21, 2012 at 07:40 AM
First off, from my point of view, lazy sequences are the moral equivalent, in data structures, of i/o. While in one sense, they are "pure", they have an inherently time-dependent nature and an external dependency which you need to eliminate before they can be completely valid:
Hypothetically speaking, if you evaluate [1..] it's going to stop sooner or later. It will never reach infinity -- you'll get a machine failure before then if you let it run long enough. The valuable part of this expression, then, is not the generator that counts up indefinitely, it's the (contained) function which maps generator state to values.
So, anyways, this dichotomy between what I see as the useful parts of a lazy system and what I see as the parts that are i/o related make talking about the relationship between OO and FP a bit stilted for me.
That said: it has been my general experience that pushing regular, simple computations into my computational "leaf nodes" and bringing irregular "application specific" computations up to towards my computational "root nodes" tends to result in a simple to understand and maintain system and a system that performs well.
But, also: in my experience, the "information hiding" aspect of "encapsulation" tends to result in sub-optimal modularity. If nothing else, it's hard to debug hidden information. But sometimes "optimal modularity" is not a worthwhile goal. For example, when work involves "independent administrative entities" you need clear agreements on how they work together, and you need well defined interfaces and standards. And, OO can be a really good model for how to design these interfaces. If you can make FP fit the problem, it can be an even better model (simpler to work with, and simpler to understand), but that depends on the people you are working with and the nature of the problem -- it's not always practical to completely specify all of the relevant information that the system depends on.
Anyways, ultimately the "goodness" of a system is a people issue as much as it's an architectural issue.
Posted by: Raul Miller | March 21, 2012 at 07:56 AM
The similarity may not be so obvious, but you come back to essentially describing Erlang's model. Autonomous, concurrent, message passing objects (oops processes) each being implemented using FP constructs including full immutability. The curious part about erlang that this doesn't talk about is the state. Erlang is able to maintain state despite completely adopting immutable constructs, by maintaining state on the stack.
Posted by: Dhananjay Nene | March 21, 2012 at 09:09 AM
Amen! Widespread synchronous "message sends" are really a red herring.
Another gradient that agrees is typing. Functional languages generally emphasize simple container types, and non-nominal composition (tuples, ocaml/clojure structual objects/maps, prevalent untyped usage of lisps). Up close, if you've two lists of strings, say one of first names and one of last names, there isn't much chance of getting them confused. You may even be passing them both (independently) to the same function, as they're both lists of strings (the higher purpose is still within your mind).
However at the module level, the behavior becomes so complex, and the data structures opaque (de facto through complexity and de jure through published interfaces), that the *name* is really the only thing that identifies what a type is used for. We don't really want to see a (Map(String, (String, String, String)), we *want* the semantic encapsulation and documentation of the abstraction (note that the composite type I gave isn't really that complex, but it's not a stretch to imagine an entire hierarchy of structural values that don't give much clue as to their invariants or purpose).
Regardless of the philosophical reasoning, new ("multi-core") languages will be leading us there anyway. Immutability and actors are both promising solutions to parallel processing, and I can only expect to see more languages incorporating them as first-class constructs. Whether communication is better phrased in terms of dually-owned channels (Rust/Go/Felix/OCaml) or implicit per-object mailboxes (classic object orientation, but asynchronous) remains to be seen.
Posted by: mindslight | March 22, 2012 at 12:44 AM
Alan Kay has said that Actors are closer to his original conception of Objects. Synchronous messaging was an early implementation choice based on limitations of then-current hardware. Messages were always the key concept. As previously noted, asynchronous messaging is closer to the biological metaphor.
Functional and Object/Actor-based programming are actually quite compatible when BOTH have first-class status within a system. I believe that a lot of difficulties have resulted from trying to force everything into one model or the other. For example, when mostly-functional languages introduce "expressions" that cause effects, breaking their purity.
We need both stateful and stateless constructs, cleanly separated from each other, but interoperable. This is the approach I've taken with Humus [1]. Expressions are purely functional, yielding immutable values. Actors encapsulate mutable state, acting like Objects, but using asynchronous messaging (tell, not ask).
Ironically, even pure-functional expression evaluation can be expressed with asynchronous actor messaging [2]. However, this detail can, and often should, be hidden from the programmer. From a conceptual standpoint, we want to reason about pure functional values (below), as asynchronous messages among stateful Actors/Objects (above).
[1] http://www.dalnefre.com/wp/humus/
[2] http://www.dalnefre.com/wp/2010/08/evaluating-expressions-part-1-core-lambda-calculus/
Posted by: Dale Schumacher | March 22, 2012 at 06:51 AM
This reminds me of "Parameterise from above" as in http://accu.org/index.php/journals/1411
Posted by: Bob Corrick | March 22, 2012 at 10:47 AM
Essentially isn't this what the IO Monad is in Haskell? It forces the separation of pure functionality from impure functionality. You often end up with an impure layer on top to orchestrate things, with the a large amount of pure code underneath.
Posted by: Nathan | March 22, 2012 at 11:09 AM
Default Java(such as spring or JavaEE) is a wrong OO system, I found open source Jdon Framework is one of many Java message/event frameworks, it brings "Tell, Don't Ask" to java developer.
Posted by: Banqjdon | April 22, 2012 at 07:31 PM
It isn't clear that functional programming is necessarily lazy. Bob Harper disagrees: https://existentialtype.wordpress.com/2011/04/24/the-real-point-of-laziness/
Posted by: Vincent Toups | April 23, 2012 at 02:58 PM
Nice post Michael! As i read it, it immediately reminded me of a presentation of Tim Sweeney (Unreal Engine tech Lead) where he expresses his "wishful thinking" of a functional-o.o hybrid language and which contexts coding in one or other paradigm makes sense. Nice to see the ideas converge in a certain way: http://www.slideshare.net/jstrane/tim-sweeneys-invited-talk-at-popl06
Posted by: Account Deleted | April 25, 2012 at 06:14 PM
baidu
[url=http://www.sina.com]sina[/url]
Posted by: sxm201207 | September 12, 2012 at 06:14 PM
I'm a big fan of Gary Bernhardt's 'functional core, imperative shell' idea as described in this talk: https://www.youtube.com/watch?v=yTkzNHF6rMs
It proposes something similar as a way of making everything more unit-testable, but it seems to work as a more general architectural design as well.
Posted by: Mark Jordan | July 29, 2014 at 09:29 AM