In the following articles, I want to go through the typical (and not so typical) functions which are used to transform collections — map
, filter
, groupBy
etc. Often, I have been confronted with the view that using these functions amounts to functional programming. I’m not sure I agree with that view, similar to how I’m not sure I would agree that changing tires constitutes being an auto-mechanic.
However, since it is at least possible that many readers will share this preconception, I will take advantage of the opportunity to give some general thoughts on functional programming which might be useful to newcomers, and help them incorporate the term into their mental frameworks.
FP vs. OOP?
It is often the case that the techniques of FP (Functional programming) and OOP (Object-oriented programming) are viewed as mutually exclusive — that you somehow have to use one or the other. People from the OOP camp view FP as a kind of black magic filled with big bad words like functors and monads, while people from the FP camp view OOP as the easy way for the stupid, primitive masses which are completely ignorant about “the higher way”. Since OOP has been in the mainstream for a longer time, the OOP camp is larger. It is interesting to realize that, for the same reason, most of the people in the FP camp started in the OOP camp — rarely is the opposite true.
However, I would like to present a different view. I think that this battle between camps is nonsensical, because the two are not mutually exclusive. In fact, not only can you use both FP and OOP, you absolutely should! The reason is this: in a sense, OOP and FP are both a consequence of the same fundamental design techniques, just applied to different design problems.
The design techniques we are talking about are ones that you apply every day, without thinking:
- Decomposition: When building a big thing, decompose it into comprehensive smaller things
- Abstraction: When you encounter a repeating pattern, create an abstraction that expresses it
So, let’s use these techniques to tackle the following problem:
Design an application that exposes an API to accept some type of data (a text file, a string, a list of strings, …), performs some type of analysis (count words, semantic analysis, whatever), and saves the result to a database.
The exact details are not important for our discussion.
OOP
Immediately, even as you were reading the sentence above, you were already decomposing the application into smaller components — there’s going to be a controller, a DBO, a service layer, etc. If you didn’t use a framework and wrote the whole thing from scratch, you would certainly encounter many patterns that you would express using an abstraction — repeated code that defines endpoints, processes requests/responses etc. (a HTTPController
), code that validates the different kinds of input (a Validator
), etc.
All those things, those objects, have behaviors that are useful to the problem we're trying to solve. Of course, this is exactly what OOP is about, but what we want to emphasize is that designing things in an object-oriented manner is really a consequence of the two techniques we talked about (decomposition, abstraction), along with the fact that thinking in terms of objects and behaviors is something that we as humans feel comfortable with.
Now, if we were hardcore members of the OOP camp, the designing phase is likely over. We’ve decomposed the problem into a bunch of objects, so now it’s time to roll up our sleeves and go build them. And for the most part, that means writing methods. So we do that, writing method after method, building each method by implementing its atomic steps using lots of for
-cycles and if
s and assignments and stuff.
FP
Here’s my question: why? Instead of approaching those methods as a design problem and using the same techniques we just talked about, the same techniques that we use completely automatically when designing things, we just start ‘hacking away’. No looking for patterns, no decomposition into comprehensive units. For some reason, we tend to approach the problem of building behaviors (as opposed to things) in a completely ad hoc manner.
‘Hey, maybe we should start doing this’ is basically the core idea of FP — just use the same techniques you already do, but apply them when designing behaviors. That’s it, nothing special about it.
When you start doing that, you will notice a lot of repeating patterns, and that’s where map
and fold
and all the other functions come from — they are names for units of code that are useful when building behaviors. Every single one of those units can (and is) implemented using a combinations of if
s and for
-loops, but contrary to if
s and for
-loops, they have a name which communicates what they do.
Simple FP example
Take a look at the following:
//sampleStart fun getVariableSymbol(plan: FinancialPlan) { if(plan.buckets != null) { for(bucket in plan.buckets) { if(portfolioId == bucket.remoteId) { val variableSymbol = bucket.variableSymbol ?: fetchVariableSymbol(clientId, bucket.remoteId) if(variableSymbol != null) { bucket.variableSymbol = variableSymbol accountOperations.save(account) } return variableSymbol } } } return null } //sampleEnd
Read through the code until you understand it, and take note of the time that it takes for you to get an idea of what this code is doing.
Now compare it with this version:
//sampleStart fun getVariableSymbol(plan: FinancialPlan) = plan.buckets ?.find { it.remoteId == portfolioId } ?.let { bucket -> (bucket.variableSymbol ?: fetchVariableSymbol(clientId, bucket.remoteId)) ?.also { bucket.variableSymbol = it accountOperations.save(account) } } //sampleEnd
Immediately, in 0 seconds, you know that the code looks up a bucket from the financial plan, transforms it, and returns the result. It will take longer to figure out what exactly the transformation does and other low-level details, but from the very start, you have a framework in your mind of what this code does. And the generic part (i.e. the find
) is also provably correct, as opposed to the previous example, where you could have made a small mistake.
This really is everything there is to know about FP and what its purpose is. Even the more advanced concepts (which are completely beyond the scope of this text) are nothing more than this.
For example, think about how many times you wrote a data structure that had a value “inside” it — a List
, a Map
, a Tree
(all those have 0 or more values), but e.g. also an Optional
(may or may not contain a value), or a Deferred
/Promise
(a value that will be there in the future).
All of those data structures, and many, many others, have the nice property that there's a very natural way to "take the value out", transform it, and "put it back". In the context of List
/Map
/Tree
, that means creating a new List
/Map
/Tree
with the transformation applied to every element, in the context Optional
, it means creating an Optional
out of the transformed element if it's there, or just keeping Optional.empty()
if it's not and with Deferred
/Promise
we create a new Deferred
/Promise
which applies the transformation to the result of the original Deferred
/Promise
.
This is a pattern that appears over and over and over until someone got tired of calling it the "thing-with-transformable-insides" pattern and gave it a name - functor.
Structure programs using OOP, implement behaviors using FP
To sum up, OOP and FP are nothing more than the result of applying the same design technique to different problem scopes:
- OOP tells us how to decompose our application into objects with behaviors, and allows us to logically structure our code into files and packages
- FP tells us how to decompose the behaviors of those objects into fundamental building blocks
You can see that it’s not about using one or the other — it’s about using one and then the other. Structure your programs using OOP, and implement behaviors using FP.