Your Programming Toolbox: Functional and Object-Oriented Paradigms
Daniel Orner
Posted on November 27, 2020
Object-oriented (OO) and functional programming (FP) are the two most popular programming paradigms in use today. Most languages provide at least some features from one or both of them. Although I don't intend to dive into what exactly OO and FP are, as there are many, many articles already written that attempt to do this, I do want to try to weigh in on a corollary argument: "Which one's better?"
There have been countless keystrokes shed in pursuit of this age-old question. For example, we've got Ilya Suzdalnitzky proclaiming the end of OO as we know it, and a number of other articles passionately arguing the opposite.
This combat area has been re-tread so often, it's practically a no-go zone. The most thoughtful articles on this are can be distilled into the following insights:
- Any paradigm can be done well, or badly.
- Both FP and OO have advantages in certain cases and disadvantages in others.
- Both FP and OO have wide-ranging definitions, and depending on what you're doing and in what language, you may think you're doing one or the other but someone else may disagree, or maybe you're doing a mishmash of them both.
As time goes on, it becomes clear that many popular languages are diving deeper into becoming multi-paradigm - they allow you to write in procedural, OO or FP and you can pick and choose your language features.
Because of this, you can take bits and pieces of these different approaches and try to synthesize them into the best of both worlds. I'd like to touch on my thoughts on what the good pieces of the two approaches are, and when to use the rest.
The Human Touch
One of the major differences between OO and FP is in the visceral, intellectual way that people see code. In OO "everything is an object"; in FP "everything is a function". Of course, neither of those statements is entirely true. But at a high level, people who are more mathematically inclined seem to enjoy thinking of code as functions, while those less so find it more intuitive to think of code as objects.
I personally fall into the latter camp - my first task in any programming problem is to think of the "things" and after that, what has to happen with those "things". Because of this, my starting approach is OO, and then I start thinking of how I can use aspects of FP to improve some of the shortcomings of it.
How functional?
In my opinion, the power of FP comes from thinking about data as immutable and functions as pure (or pure-ish). The main benefits are testability and confidence. You can write your tests knowing that given a particular input, you will always get the desired output, since you don't need to worry about the state of the object you're calling it on.
Having said that, I do not advocate a complete FP approach unless your whole team is onboard with that, because then you have to wade into the waters of monads and functors and higher-level functions and functional math (ew, math). I think this article, written about JavaScript, gives a really good overview of how to take the good parts of FP without being slavish.
My take on pure functions is more along the lines of: "Your function should either be pure, or accomplish something in the real world, but not both."
The Nitty Gritty
I'll be using Ruby in my examples, as it's the multi-paradigm language I'm most comfortable with, but it's almost always possible to convert these examples to JavaScript, Java, Python, etc.
One of my earlier revelations about FP and OO is that from a technical perspective, they are practically interchangable. If you think of your object as data, and the functions/methods as acting on that data, then the following two chunks of code are equivalent - except that the first is in OO-land and the second is in FP-land.
Person = Struct.new(:name, :status) do
def marry!
@status = "married"
end
end
person = Person.new("Daniel Orner")
person.marry!
person.status # married
Person = Struct.new(:name, :status)
def marry(person)
Person.new(person.name, "married")
end
marry(Person.new("Daniel Orner")).status # married
They accomplish the same thing - except that in OO-land, the function lives with the data and changes the data, while in FP-land, the function lives outside the data and returns a new set of data.
Most important things that can be achieved by one paradigm can be achieved by the other as well, as I'll explore below.
Code Organization
One use of classes is a way to organize your methods. If all your methods dealing with a person are in the Person class, it's easy to parse out where to find the thing you're looking for.
FP has a corresponding way to do this as well, although it's more language-specific. In JavaScript, you might put all your functions in a single file. In other languages, you might use packages, namespaces, modules, etc. and ensure that you use "static" or "module-level" functions.
## OO-land
class Person
attr_accessor :first_name, :last_name, :address
def move_to(address) # ...
def marry(person2) # ...
end
## FP-land
class Person
attr_accessor :first_name, :last_name, :address
end
# person_operations.rb
module PersonOperations
def self.move_to(person, address) # ...
def self.marry(person, person2) # ...
end
This leaves the Person class as a value object - it's more of a type than a class - and all operations live outside it.
Encapsulation
With OO, you can have private state which isn't exposed, and you only expose essentially an API for external code to call. How could you do that with FP?
The short answer is, not easily. You can use closures to achieve some level of encapsulation, but it will largely be on a global level rather than encapsulating data in an individual object. This is one of the weaknesses of FP.
However, the bigger question is why you are encapsulating data in the first place. If you're using FP, your functions are pure and your data is immutable - so you can't have anything accidentally or maliciously change your data.
The main reason I see to encapsulate data is as a form of documentation: This class has ten fields, but only five of them are relevant to anything outside the methods in this module that do these things to it. (Honestly, once you've even had this thought, it usually turns me to "do these five fields really belong with this data, or should they be somewhere else?")
Composition
One of the basic tenets of functional programming is using composition to combine functions together. For example, here's a way to take two functions and combine them into one that does both things:
add_one = ->(x) { x + 1 }
double = ->(x) { x * 2 }
double_and_add_one = add_one << double
double_and_add_one.call(5) # 11
In OO-land, composition means modeling things as a "has-a" relationship instead of an "is-a" relationship. For example, instead of treating an Employee as a subclass of Person, you might instead give your employee an attribute of type Person. It's not as intuitive, but it works just as well while avoiding some of the pitfalls of inheritance.
class Person
attr_accessor :first_name, :last_name
def initialize(:first_name, :last_name)
@first_name = first_name
@last_name = last_name
end
def full_name
"#{@first_name} #{@last_name}"
end
end
class Employee
extend Forwardable
attr_accessor :person
def initialize(first_name, last_name)
@person = Person.new(first_name, last_name)
end
def_delegators :@person, :full_name
When to use what?
In general, you can use what I feel are the "important parts" of FP easily within an OO framework, but not so much the other way around. For example, I like the idea of value objects. My sweet spot is leaving all functions related to reading or writing attributes in the class, but anything bigger than that outside of it.
But OO has its advantages as well. When should you:
- Have methods live with your data
- Provide an inheritance structure
- Allow overriding of "super" methods
I've found two really good cases for these:
- Framework code
- Value object hierarchy
In all other cases, I'd put a big question mark on it.
Framework code
When I use the term "framework", I mean you are defining a way to accomplish a particular task without actually doing that task - generally when you have to have many slightly different ways of doing the same thing. Some examples from my past include:
- A web scraper that saves data to a particular schema/model
- A producer that takes data and maps it to Kafka topic
- A CSV parser that needs to be able to handle different formats for different clients
In all these cases, you want to have some "base behavior" defined which you can easily use, but you want to be able to override bits and pieces, and provide helper methods.
Using a pure FP approach in this case is definitely possible, but a lot of work, because the "overriding" piece needs to happen via the use of composition and hooks.
Here's a sample parser (without showing method implementation) that takes in a CSV file and outputs a structured array to save to the database, in OO-land:
class Parser
def parse(filename) # ...
def read_file(filename) # ...
def parse_headers(header_row) # ...
def parse_row(row) # ...
def clean_up_data(structured_array) # ...
def results # ...
end
Using this framework, you can write a parser only by overriding the two methods that every parser needs to write, parse_row
and parse_headers
. The other functionality "just works" since it uses the parent code.
In FP-world, you'd need to implement it similar to this:
module Parser
def self.parse(filename, parse_header, parse_row)
def self.read_file(filename) #
def self.clean_up_data(structured_array) # ...
end
You'd need to pass in the parse_header
and parse_row
functions to the parse
method to be able to override this behavior.
This example doesn't look too bad, but what if there are additional steps you want to add functionality to? Inheritance, and particularly the super
keyword, allows you to easily "wrap" a function around specialized code without having to do anything special on the parent class:
class MyParser < Parser
def clean_up_data(row)
new_row = handle_special_characters(row)
super(new_row)
end
In FP-land, you'd need to do something like this:
module Parser
def self.parse(filename, parse_row, parse_header, clean_up_data)
...
if clean_up_data
row = clean_up_data(row)
end
internal_clean_up_data(row)
end
And if you want to "wrap" the original behavior, you'd need to grab a reference to the function on the module that does it and call it:
def clean_up_data(row)
my_row = handle_special_characters(row)
Parser.method(:clean_up_data).call(my_row)
end
Parser.parse("my_file", my_parse_func, my_parse_header_func, clean_up_data)
As your framework grows in functionality and usefulness, you'll find more and more of these overrides are necessary as the use cases expand. If you don't want to build them all into your own code, you need to allow people to make changes as necessary, and OO allows a much cleaner way to do that.
Inheritance in Value Objects
I think there is also space for OO-style inheritance when dealing with your data, as long as you can absolutely model that data as an "is-a" relationship. The main difference is that your classes are things that are, not things that do. You can do this with a Service
, Employee
, House
, etc. - not with a Parser
, Manager
, or DatabaseLayer
.
For example, if a user has a name, e-mail address and login, and so does an employee, it makes sense to have Employee inherit from User since you get its fields for free, and you can safely say that a) every Employee is a user, and b) Employee would never change the behavior defined in User in such a way to violate the Liskov Substitution Principle.
You can also freely use interfaces or abstract classes in languages that support it, and approximate them in languages that don't (e.g. defining a class in Ruby where every method consists of a body that raises a NotImplementedError
).
However, I'd consider it a code smell going past that in non-framework code.
Final Thoughts
When I was first introduced to FP a few years ago, I had a hard time wrapping my head around it because so much of the discussion around it felt so academic. As I've come to understand the real-world benefits of using it, I'm more able to reap those benefits without entirely overhauling how I work. I'm still feeling my way towards the golden road of combining the features of these paradigms in the right ways in the right cases.
Hope this is helpful to others out there as well!
Posted on November 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
June 18, 2019