Functional Programming Is a Leaky Abstraction
I love functional programming patterns, but why don’t pure functional programming languages have more mainstream traction? I believe that it’s because they are a leaky abstraction.
What do you mean by pure functional programming?
Functional programming is when an algorithm is expressed using only functions, as opposed to imperative programming where it is expressed using sequential statements. Pure functional programming often adds the restriction that functions cannot mutate data or otherwise have side effects. Functional programming has advantages in being able to reason about what is happening in a program as well as re-usability and therefore can help do more with less code, but it is often considered harder to grasp and harder to read because of the way it inverts the reading order of many operations. Sometimes this is pointed to as the reason functional programming doesn’t get mainstream adoption (except in mixed-metaphor languages like JavaScript, Python, or Scala), but I think it is because pure functional programming is a leaky abstraction.
What is an abstraction?
A programming abstraction is a conceptual divide between what we manipulate directly, and the end result or what is actual executed. For example, an API is an abstraction between the consumer of a set of data and the production, storage, and modification of that data. Abstractions are often used to create divisions of responsibility within a program. Programming languages are themselves an example of an abstraction. A high-level language hides assembly or byte code, which itself hides machine code. In the end, all languages have to cause different machine codes to be executed by a processor (the end result), but usually you write your program in the highest language, very rarely do you write things in a lower abstraction level.
What makes an abstraction leaky?
An abstraction becomes leaky when how you interact with it depends on its implementation. For example, a file system interface might be implemented either locally or as a remote resource, forcing you to consider latency and network availability effects when that is not an explicit part of the interface. Or a query language abstracts away database access, but two queries that give equivalent results may have drastically different performance. The abstraction was intended to hide part of the system, but you end up having to think about it anyway.
Why is leaking a problem?
Most abstractions end up leaking a little, either because of bugs, or gaps in the specification, or from being used for unintended purposes (Web technologies get that a lot). But when an abstraction leaks so much that it makes the abstraction meaningless, or actually makes coding harder or more error-prone, it becomes a real problem. For example, when Android was created they decided to implement their own version of the Java virtual machine in order to leverage the popularity of Java. But the Android virtual machine implementation introduced severe limits on the number of methods you could have in a single application, which made it difficult to use common constructs, like enums, that generated large numbers of methods. Instead of making it easier to reuse existing code, it forced new programming styles and awkward work arounds for many years.
What does this have to do with functional programming?
It all comes down to one thing: mutable state. Pure functional languages are based in lambda calculus and do not permit side effects or mutation of data, only inputs and outputs. Our current computer systems, however, use the von Neumann architecture, which uses a continuous cycle of read, operate, write steps to execute programs. Fundamentally, all functional languages are translated to an imperative language, and it leaks.
It leaks when you need to read and write files, when you need to respond to real-time user events, when you write to the screen or interact with the GPU, or when you communicate with an external process or API.
Except in the most basic batch operations, computer programs are useless without some form of state change in the system. How else do you read the result? And most interactive applications would be impossible to build without reading and writing files, updating screens, or communicating across a network, all inherently stateful operations.
But what about monads?
Monads are often presented as the solution to stateful operations, but are the largest indicator that the abstraction is leaking.
At a high level, monads represent ‘the state of the world’ as an input or output to a function. This state is passed around to mark the flow that is causing external interactions. Simple, problem solved. But as a practical construct they tend to be annoying to work with and introduce a lot of boilerplate passing around of monads. Adding a simple logging statement deep in a tree is now the work of changing the entire call stack, or you pass around monads you don’t know you need yet. Some languages make special cases for that kind of situation, but then you are back around to leaking.
But I thought you liked functional programming?
I really like using functional patterns. There are many cases where they work better and are just more elegant than object-oriented or imperative patterns. Functional programming can solve a certain class of problems in beautiful ways. But the world is stateful, computers are stateful, and it is already hard to grasp their inner workings. A leaky abstraction just adds to that, and I don’t think it will ever be overcome. But I love that so many features of functional languages are moving over, like match statements, first-class functions, closures, and more. I also think that dataflow programming provides a great hybrid, which I plan to write about in a future post.
So here’s to functional programming in an imperative world!