I have two big problems with the “functional vs. object oriented” debate.
I addressed one of them in a previous post: imprecise language. That post disambiguates six different terms that we use interchangeably to refer to programming paradigms, problem-solving approaches, and language characteristics.
But in that post I also introduced another issue:
The “all or nothing” assumption. I have a colleague who wants all his code to be functional code, full stop. I also have two former colleagues who swear by, and I quote, “lots of little objects.”
Presenting someone with two choices and asking them which one is universally superior makes for entertaining intellectual cage matches, but I think a more nuanced question leads to more insightful answers. Suppose that both approaches had value. What makes each of them valuable?
To answer this question, let’s look at an example where we accomplish similar functionality with each of them.
One Problem, Two Implementations
Suppose I’m writing software to model a menagerie of birds, and I have a class for each of my bird species. I also have a function called
eye_contact_with that takes in an instance of one of my Species objects and calls the
respond method defined on that species, like so:
class RedShoulderedBlackbird(): def respond(self): print("I'm going to land on your head, scream at you, then steal your hot dog") class BigBird(): def respond(self): print("I'm going to teach you a letter of the alphabet!") class Parrot(): def __init__(self): self.annoyed_level = 0 def respond(self): self.annoyed_level += 1 if self.annoyed_level < 3: print("I'm going to bite you") else: print("OKAY THAT'S IT BUCKO, YOU'RE GETTING THE BEAK!!!!!!!") def eye_contact_with(bird): bird.respond()
For simplicity, let’s just say it’s guaranteed that every species of bird will have its own
I can make an instance of any one of my bird species:
red_shouldered_blackbird = RedShoulderedBlackbird() big_bird = BigBird() parrot = Parrot()
Then I can pass any of them to
eye_contact_with and see responses:
>>> eye_contact_with(red_shouldered_blackbord) >>> "I'm going to land on your head, scream at you, then steal your hot dog" >>> eye_contact_with(big_bird) >>> "I'm going to teach you a letter of the alphabet!" >>> eye_contact_with(parrot) >>> "I'm going to bite you"
Now, objects allow us to intermix two things: state and behavior. We have some behavior represented here in the
respond methods. We could add more behavior by adding additional methods like, say,
Do we have any state? We do. In this case, the
Parrot class has state: in particular, am attribute called
annoyed_level. Every time somebody looks at the parrot, its
annoyed_level increases, until finally…
>>> eye_contact_with(parrot) >>> "I'm going to bite you" >>> eye_contact_with(parrot) >>> "I'm going to bite you" >>> eye_contact_with(parrot) >>> "OKAY THAT'S IT BUCKO, YOU'RE GETTING THE BEAK!!!!!!!"
It loses its cool and goes on a biting rampage. Without outside intervention to calm it down, the bird stays angry.
In this case, I have integrated state with behavior by modifying the
Parrot object whenever one of its methods is called. Through this object-based approach, I keep track of the state of the menagerie by having each bird store and manage its own emotional state, rather than keeping that information in some sort of external register. Convenient, right? It is, but it can go south in two different ways:
- Calling the same method on the same parrot ultimately produces different results, which might confuse developers who weren’t expecting that. For example, if you’re new to birds and you make eye contact twice with a parrot to no immediate effect, you might be horrified to have your third attempt at friendship end in a bite-fest.
- Scope mistakes made in object-oriented programming can lead to the objects getting modified in unexpected ways. When that happens, it can mess up the whole system, since the system relies on objects managing their own state. For example, the current implementation of
Parrotdoesn’t expose any kind of function to calm the bird down. So now, any zoo-goer who glances this parrot’s way is in for a rude surprise by no fault of their own.
Compare the above to a function-based solution with similar behavior:
def obtain_response(bird, stimulus): stimulus(bird) def eye_contact(bird): if bird == "Parrot": print("I'm going to bite you") elif bird == "RedShoulderedBlackbird": print("I'm going to land on your head, scream at you, then steal your hot dog") elif bird == "BigBird": print("I'm going to teach you a letter of the alphabet!")
Instead of several classes and a one-line
eye_contact_with function that calls the
respond method on whatever got passed in, we have a function that itself distinguishes the type of bird with whom the caller has made eye contact. Rather than individual birds managing their own responses, the method does it. That method is one of any number of methods that might be passed into
obtain_response and then called on the bird. Another stimulus might be
feed, for example.
For the first interaction with each of these birds, the behavior mimics that of the object-based approach:
>>> obtain_response("red_shouldered_blackbird", eye_contact) >>> "I'm going to land on your head, scream at you, then steal your hot dog" >>> obtain_response("big_bird", eye_contact) >>> "I'm going to teach you a letter of the alphabet!" >>> obtain_response("parrot", eye_contact) >>> "I'm going to bite you"
The difference is, there’s no state here. So, no matter how many times someone calls
obtain_response("parrot", eye_contact), the parrot will never come for anyone’s flesh—at least not in the current implementation. Functional programming tutorials refer to this as “stateless” behavior and applaud it as a great way to eliminate the two drawbacks to object-based approaches that we discussed above.
When functional programming enthusiasts refer to a function like this as “stateless,” what they mean is “calling the same function with the same inputs will always produce the same outputs.” I prefer to say idempotent for this behavior rather than “stateless,” because “stateless” makes it sound like there is no state. And that’s usually not true.
Functional programs are “stateless” the same way that “serverless architecture” is “serverless”. Serverless architecture doesn’t just run on etherial magic computer fumes. It runs on servers that live somewhere else. There are still servers. Functional programming is also not stateless; rather, the state is stored somewhere else. Often, a database factors in here. For example, if we wanted functional code and angry parrots, we would need to store off parrots somewhere outside the functions and then pass their
annoyed_level into the
obtain_response function as an argument.
What is each approach good for?
This is the shorthand I use to remember when each approach might be useful to me:
- Imperative programming is often particularly useful for describing and directing the interactions between different concepts. In practice, this usually amounts to object-based solutions.
- Declarative programming is often particularly useful for manipulating collections of data where the datums are independent (not specifically interacting with one another).In practice, this usually amounts to function-based solutions.
Those two ideas help to explain a lot of things about our favorite frameworks:
- Many web frameworks (like Rails and Django) and mobile frameworks (like Android and iOS) have you inherit from objects. The superclasses can handle all these objects’ instantiation and relationships with each other while you happily add in what the pages should look like or which data the endpoints should fetch.
- Golang (a functionally-oriented language) hasn’t gotten as much traction as a language for writing APIs or web apps, and the tooling around doing so with Golang is about as mature as it is for Swift, an object-oriented language that is seven years younger.
- SQL (a declarative language) is used for fetching collections of data out of databases!
- Functional approaches are popular in extract-transform-load (ETL) pipelines that do something to a bunch of data and then move it somewhere.
- Functional approaches are popular for transforming a bunch of data pulled from an API before displaying it.
Because of those last three examples, people often get “functional programming” and “functions that specifically manipulate lists” mixed up—in fact, most Python resources do this. If you look up “functional programming in Python” 95% of what you find is “how to use map and filter, tada!” But
filter are not, by themselves, functional programming. Yes,
filter are idempotent functions. But so is
__add__. And none of those methods dictates the problem-solving approach of the programmer.
So how do I choose the appropriate problem-solving approach for the task at hand? We’ll get to that in the next post.
If you liked this piece, you might also like:
The last time I just absolutely snapped on imprecise terminology in tech (on this occasion about “technical debt”)
The debugging category (people seem to like this and struggle to find similar content elsewhere)
The risk analysis workshop (4 out of 5 “Jimi Hendrix of [insert programming language here]”s approve!)