Making the case for functional programming
Where does the difference between Java and Haskell come from?
KO | EN
Functional programming is clearly mainstream now. Many programming languages have already adopted its core ideas, and companies like Greenlabs are producing successful examples of functional programming at enterprise scale. Books and other materials explaining functional programming are easy to find too. Lately, even people just getting started in programming seem to recognize its importance.
And yet there still are not many programmers who take functional programming seriously. Programmers who have only just started looking into what it is often doubt themselves: “I use map, filter, and reduce every day, so am I a functional programmer?” In a broad sense the answer is yes, because they handle first-class functions and preserve the immutability of lists, but that still does not wash the doubt from their minds cleanly. By the same token, saying “the language you use every day is a functional programming language, and we are all doing functional programming” rings hollow. That may be true, and it may lower the psychological barrier around functional programming, but it still is not what people expect functional programming to be.
Meanwhile, ideas like functors and monads are still taken as the kind of thing only programming language nerds dig into. Haskell can even seem more like mathematics. Listing the many advantages of functional programming and talking as if it were simply the right thing to do does not persuade anyone. As John Hughes put it in Why Functional Programming Matters[1], “The functional programmer sounds rather like a medieval monk, denying himself the pleasures of life in the hope that it will make him virtuous.” If functional programming is to be persuasive, as Hughes said, it has to prove its practical benefits.
Functional programming as a paradigm
The first thing people do when trying to understand functional programming is look for the languages called functional programming languages. What counts as a functional programming language? It is a perennial circular debate among programmers. The debate always ends up being wasteful because there is no agreed standard or definition of functional programming. In a broad sense, though, any language that supports first-class functions is a functional programming language. If a language lets functions be treated the same way as other values, that is, if you can assign a function to a variable, pass a function as an argument to another function, and return a function from a function, then that language supports first-class functions. Things often mentioned as the core of functional programming, such as pure functions, immutability, lazy evaluation, referential transparency, and currying, are secondary by comparison when seen from the perspective of language design.
By that definition, JavaScript, Python, and Java, all of which support first-class functions, are all functional programming languages. It is genuinely possible to do functional programming in them. But if Java really is a functional programming language like Haskell, where does the subtle difference people feel when comparing Java and Haskell come from? Perhaps the reason the argument over what counts as a functional programming language keeps returning, even though there is an agreed definition of first-class functions, is that this subtle difference is not easy to explain.
Misunderstandings about Haskell contribute to that subtle difference, but I think the bigger gap lies in the paradigms the two languages aim at. A programming paradigm is not exactly the same thing as the scientific paradigm Thomas Kuhn talked about, but the idea can still be borrowed, in the sense that programmer communities in each era tend to share a common set of theories, rules, beliefs, and values. To talk about functional programming as a paradigm, it is not enough just to explain what functional programming is. We also need to look at the historical context through which programming paradigms have developed.
The ancestor of what we now call a function in programming was the subroutine. A subroutine was simply a way to jump the program’s flow of execution from the main routine into a subroutine. At the time, a subroutine that returned a value to the main routine was called a function. Later, the distinction between functions and subroutines disappeared, and all subroutines came to be called functions. This kind of function is different from a function in mathematics. A function
The goto statement, which transfers control flow to another subroutine, created the problem of execution jumping around wildly. Out of that concern came the paradigm of structured programming. In Go To Statement Considered Harmful[2], Edsger Dijkstra argued that the goto statement gets in the way of modularizing programs, and that every program can be expressed with only three logical structures: sequence, selection, and iteration. Even today, when many programmers first learn C, they are warned not to use goto, and that all comes from the paradigm of structured programming. The reason most modern programming languages do not have a construct like goto is also part of that influence.
As software grew more complex, the modularization of programs became an even more important problem. The object-oriented programming paradigm interprets a program as a collection of objects, and sees it as operating through the interactions among those objects. Smalltalk, which presented itself as a pure object-oriented programming language, built the basic framework of what we now call the object-oriented programming paradigm. Java, in turn, drove the commercial success of object-oriented programming. Even now Java is used everywhere, in companies, government, schools, and elsewhere, and because of that the object-oriented programming paradigm Java embodies came to be accepted almost by default everywhere. Over time the object-oriented programming paradigm has gone through many revisions and changes, but even now it can still be described to some extent just by listing the traits of object-oriented programming. There is no shortage of disagreement, though, over whether the traits usually treated as its core, encapsulation, inheritance, polymorphism, message passing, and so on, are really its defining traits. After all, they are not exclusive to object-oriented programming. Just as there is no agreed standard for what counts as a functional programming language, object-oriented programming seems to be much the same.
Across all of these paradigms, functions were treated as special. Unlike other values, they could not be assigned to variables, passed to other functions as arguments, or returned from functions. They still behaved differently from functions in mathematics. That made it hard to predict what would happen when a function was called. The functional programming paradigm interprets a program itself as a kind of function with inputs and outputs, and says that a single program is made up of many functions composed together. Interpreting programs this way made it possible to define software with mathematical rigor and prove properties about it. To make that possible, functions had to be “normalized” so they would no longer be treated as special. Everything we call a characteristic of functional programming starts there.
Now let me return to the question of where the difference between Java and Haskell comes from. One of the biggest ways to distinguish the identity of a programming language is the constraints it places on programmers. Both Java and Haskell make functional programming possible, but the constraints differ. Java does not enforce pure functions. Nor does it put any constraint on changing the state of variables or objects. Haskell, by contrast, imposes strong constraints. You can do object-oriented programming in Haskell too, but even that is possible only on top of highly abstract interfaces designed not to violate the overarching principles of functional programming.
A paradigm governs a language, and the ecosystem around it, including the community that builds and uses it. Java has taken in many elements of functional programming, but the dominant paradigm in the Java ecosystem is still object-oriented programming. Java’s standard library and third-party packages follow object-oriented principles, and companies that use Java teach object-oriented programming principles to new programmers. The enormous body of legacy code written in Java over the past few decades faithfully reflects the object-oriented programming paradigm. In the end, languages are distinguished by the paradigm their communities follow, along with the constraints that paradigm treats as right. So when people casually refer to a language as a functional programming language, they usually do not mean only that it supports first-class functions. More often they mean that its ecosystem aims at the functional programming paradigm.
Languages that support several programming paradigms are sometimes called multi-paradigm languages. Scala, for instance, combines the constraints of procedural programming with those of functional programming. That sounds awfully enticing, as if it had gathered only the strengths of several paradigms, but the term multi-paradigm language does not mean very much in itself, because almost every modern programming language adopts multiple paradigms of one kind or another. These days, when people say a language is multi-paradigm, they usually mean one of two things: either the language supports functional programming well, or a functional programming language is trying to appeal to a mainstream audience.
The practical benefits of functional programming
Even among programmers who do not take functional programming seriously, quite a few are at least curious about what a monad is. There is a myth that once you understand it, you can be reborn as an “advanced programmer.” That is why there are so many articles explaining monads. But what does it really amount to? In the functional programming paradigm, monads are a lovely concept, but to programmers who encountered monads before functional programming, they feel more like something frightening. Even after understanding that a monad is just a monoid in the category of endofunctors[3], it is still not easy to persuade other programmers of the practical benefits of functional programming. If the goal is to persuade programmers peeking into functional programming that monads are useful, it may not be the best approach to explain functors, monoids, and monads one after another with “even an elementary school student could understand this” examples about putting values into boxes and taking them back out.
At that point, readers run into exactly the same problem that beginners run into when they study object-oriented programming. To programmers who have never built a large application, concepts like classes, encapsulation, and SOLID all look unnecessary. Even after they understand those concepts, they still do not get an answer to the question, “So what?” The mathematically rigorous concepts of functional programming are genuinely elegant and important, but they are not especially appealing to programmers who are already buried under the sprawling principles and complexity of object-oriented programming and blaming themselves for not measuring up. In an ecosystem governed by the object-oriented programming paradigm, what the functional programming paradigm lets programmers achieve naturally can also be achieved by faithfully following the Clean Code[4] principles Robert C. Martin lays out. The claim that you no longer have to memorize all sorts of tactical development principles and design patterns does not land all that well with programmers who already think in the object-oriented paradigm and work on codebases saturated with it.
From the perspective of programmers facing a paradigm shift, there is little reason to embrace a new paradigm unless it offers a direct answer to the problems they have right now. In the end, though, it tends to be one of two paths. Either they go through every sort of problem in the old paradigm firsthand, become disillusioned, and accept a new one, or the dominant paradigm of the ecosystem they belong to changes and they are forced to accept the new one. For years, programmers who advocate functional programming have put enormous effort into shifting the paradigm and making it mainstream, producing countless monad tutorials and internet articles that begin with “What is functional programming?” Now functional programming has become a paradigm that most programmers have no choice but to accept. Perhaps that was already true once the design of major mainstream languages with large market share began to reflect the functional programming paradigm. Unlike Kuhn’s scientific paradigms, shifts in programming paradigms can be destructive in one sense and complementary in another. The functional programming paradigm does not reduce everything built up by the object-oriented programming paradigm to ruins. The old software engineering maxim “No Silver Bullet”[5] fortunately, or perhaps unfortunately, foretells that there will be no destructive revolution.
The first moment when functional programming became deeply attractive to me was after I had lived through a major service outage caused by exceptions exploding all over the production environment. At the level of the programming language, there was no way at all to force the caller of a function to handle the exceptions that function might throw. If a programmer forgot to handle an exception, an API could fail unexpectedly at any time, whether because our own codebase threw it or because an external package did. In that situation, Rust’s Result type was enormously appealing. Influenced by the algebraic data type that languages such as Haskell, ML, and Scala call Either, this type returns a value wrapped in Ok when a function can return normally, and returns an error wrapped in Err when it cannot, which has the effect of naturally forcing error handling.
enum Result<T, E> {
Ok(T),
Err(E),
}fn divide(a: i32, b: i32) -> Result<i32, String> {
if b != 0 {
Ok(a / b)
} else {
Err("Cannot divide by zero".to_string())
}
}
fn main() {
println!("{:?}", divide(8, 2)); // Ok(4)
println!("{:?}", divide(8, 0)); // Err(...)
}There is a similar Option type. As Tony Hoare later looked back on his invention of null as a billion-dollar mistake[6], null has caused programmers pain for a very long time. With this type, which is usually called Maybe in other functional programming languages, it becomes possible to eliminate null from code entirely. When a value exists, you represent it by wrapping the value in Some. When no value exists, you represent it as None.
enum Option<T> {
Some(T),
None,
}fn divide(a: i32, b: i32) -> Option<i32> {
if b != 0 {
Some(a / b)
} else {
None
}
}
fn main() {
println!("{:?}", divide(8, 2)); // Some(4)
println!("{:?}", divide(8, 0)); // None
}The second moment came after I read Clean Code. Of the many principles that Clean Code lays out across 406 pages, a great number can be reached naturally by following the overarching principles of the functional programming paradigm, or simply do not need attention at all. You do not have to consciously keep the principles of Clean Code in mind while writing code, because the strong type system of a functional programming language enforces clean code for you. In that same sense, I felt the need for the functional programming paradigm just as strongly when reviewing the pile of pull requests in front of me. Code made up entirely of pure functions that do not produce side effects is referentially transparent. Referentially transparent code is easy to follow through its execution flow, and easy to read. Here, “referentially transparent” means that you can replace an expression with its value without changing the meaning of the program. Consider the function add, which adds two variables, as shown below.
add a b = a + b
main = print (add 1 2) -- 3If, as below, replacing (add 1 2) with its value 3 does not change the meaning of the program, then the function add is referentially transparent. add is not affected by anything outside itself and always returns the same result for the same arguments, which lets programmers predict its result easily.
main = print 3 -- 3Seen one way, there may not seem to be anything especially mysterious at the core of functional programming. There is only one overarching principle: functions should be treated as perfectly ordinary values. But the effect of that simple principle is enormous. I will end here by recommending a few resources that helped me understand the functional programming paradigm.
- Paul Chiusano, Rúnar Bjarnason, “Functional Programming in Scala”, Manning, 2014: An exemplary textbook for learning the concepts and applications of functional programming step by step. Even if Scala is unfamiliar, it does not get in the way much. If you are more comfortable with Kotlin, Jaeyong Jo, Myeongin Woo, “Learning functional programming with Kotlin”, Insight, 2019 is also good. It is more concise, and easier to read because you can still follow the later material without solving every exercise.
- Jaemin Hong, Sukyoung Ryu, “Introduction to Programming Languages”: The textbook for KAIST’s Programming Languages course. It has been revised steadily since it was first released in 2021. It is a book about programming language theory, but it is also a great help in understanding the functional programming paradigm. It can completely overturn the way you see programming languages, and after reading it all the way through, you may have the startling experience of seeing each line of code as a complete formula.
- Brent Yorgey, “CIS194”, 2013: Lecture notes for the University of Pennsylvania course Introduction to Haskell. The official Haskell website recommends the Spring 2013 materials, though the content varies slightly by semester. (The most recent materials are for Fall 2016.) Haskell’s ideas may feel unfamiliar, but its syntax is genuinely simple and intuitive, so it is not hard to learn. It is fair to wonder whether you really need Haskell in order to study functional programming, but I think Haskell makes a good text for learning how software, which cannot avoid being affected by the outside world, can preserve that “purity.”
John Hughes, “Why Functional Programming Matters”, 1990. (Korean) ↩︎
Edsger Dijkstra, “Go To Statement Considered Harmful”, 1968. ↩︎
Mentioned by James Iry in “A Brief, Incomplete, and Mostly Wrong History of Programming Languages.” It is often quoted as a joke that symbolizes how impenetrable monads are. ↩︎
Robert C. Martin, “Clean Code”, translated into Korean by Hae-young Lee and Jae-ho Park, Insight, 2013. ↩︎
Frederick Brooks, “No Silver Bullet - Essence and Accidents of Software Engineering”, 1986. ↩︎
Tony Hoare, “Null References: The Billion Dollar Mistake”, 2009. ↩︎