Does functional programming in Python make any sense?
You can do functional programming in Python.
Python offers support for high-order functions. We can create partial functions, compose functions, and use partial application with nothing but the standard Python library. If we add dependencies such as the python-lenses, pymonad, and OSlash, we get pretty much everything we could expect from a functional programming language.
Should we do it?
The mistakes we have made before
A few years ago, Scala was touted as a functional programming language. Of course, Scala does support functional programming to some extent, as well as supports object-oriented programming.
Everything would be fine if people used it as intended. However, some decided to turn Scala into Haskell for JVM. This was a terrible mistake.
I’m guilty as charged, too. I contributed a lot to the functional programming in Scala hype while organizing meetups to teach people about functional programming, writing articles on this blog, or speaking at conferences. Sorry!
Why did we do it?
It was fun. It was fun to attend a conference and listen to talks about category theory. It was fun to abstract the business domain away and write cryptic code.
But debugging such code wasn’t fun at all. It wasn’t fun to add a new feature to a three-month-old functional code either.
At some point, we started making trade-offs. One day, we decided that logging is not a side-effect. Bullshit. Of course, it’s a side-effect, but working with the code got easier when we allowed writing logs without IO monads.
Somehow we forgot that the primary purpose of a computer code is to solve a business problem, be easy to modify and be as cheap to maintain as possible. Functional programming doesn’t help with any of this. We achieve the opposite goal by hiding the business logic behind layers of monads and other abstractions. The software gets as expensive to maintain as possible, and people need special training to read the code.
It is as practical as if the government of Canada decided that even though they use English and French, all of the official documents and laws will be written in Polish. It would work. Polish is just another language. But the cost of using it to write Canadian laws would be enormous.
Somehow, we see how ridiculous the ideas are when we talk about natural languages. Yet translating back and forth between the domain language and functional programming notation doesn’t seem to bother anyone.
I won’t say we made fools of ourselves by over-zeal towards functional programming. But we will do nicely until a real fool comes around.
Python isn’t a functional programming language
Python isn’t a functional programming language and doesn’t even try to be one. It does support some functional programming, but we shouldn’t bend the language to do something that wasn’t intended.
Let’s type the following code in your Python interpreter:
import this. What do you see? The Zen of Python. What does it say?
“Beautiful is better than ugly.” We may say that functional programming is beautiful because it uses the mathematical properties of a programming language. Math is beautiful. So is functional programming. I agree. However, in this case, we talk about some skewed and perverted definitions of beauty. If we followed only this hint, we could get away with functional programming in Python.
“Explicit is better than implicit.” Functional programming is all about making things implicit. We hide the side-effect. We hide the computation. We abstract the business logic so much that we can’t tell what’s happening without spending hours deciphering the code.
implicitis a keyword in Scala. Of course, there is no magic in the Scala implementation, and all implicit operations follow strictly defined rules. It’s not enough to read the code to understand what’s happening. First, we need to learn how implicit operations work.
“Readability counts.” Code written in the functional programming style isn’t readable unless you undergo training. Everyone can understand higher-order functions after a five minutes-long explanation, but good luck grasping the sense of monads and lenses in less than a week.
Of course, everything becomes obvious after you spend months working with such code. After a year of functional programming, you won’t understand how you could ever write programs differently. But, getting used to something doesn’t mean it’s readable.
“If the implementation is hard to explain, it’s a bad idea.” That’s the final argument against functional programming in Python. Functional programming is hard to explain. I know it was the reason why some people used it. They felt superior to everyone who couldn’t understand functional programming.
We can use functional-programming principles in Python
Even though turning Python into a functional-programming language makes no sense, we should use the functional programming principles. They make code better. Always. Whether you write a front-end application, a data pipeline, or a shell script.
I have written already about using functional programming principles in data engineering, so let’s quickly recap why you should do it:
- Pure functions
A pure function is a function whose output is determined by the input. It doesn’t depend on any global state or anything outside of the function.
Such functions are easy to test. You don’t need an extensive test setup if you don’t need to create files or store data in a database. Also, it’s quite easy to reason about the function if it doesn’t have side effects hidden inside its code.
Most of your code should behave like pure functions even if you write classes and methods.
- Side-effect separation
Side-effects should be separated from the rest of the code.
You don’t need monads, but you need functions whose only responsibility is access to the global state: databases, file system, message queues, etc. If you read something from a database or store something, create a separate function for that IO operation and do nothing else inside them.
If your code consists of only pure and side-effect functions, your life becomes much easier. Now, you only need to compose those functions into larger blocks of code.
To foster composability, you may define classes corresponding to the entities from your business domain. Functions get easier to compose when the output type of one function fits as an input of only one other function. It’s like turning the codebase into a jigsaw puzzle.