We explain what functional programming is, explore its benefits, and look at resources for learning functional programming.
April 3, 2017 | 9 Comments |Depending on whom you ask, functional programming (FP) is either an enlightened approach to programming that should be spread far and wide, or an overly academic approach to programming with few real-world benefits. In this article, I will explain what functional programming is, explore its benefits, and recommend resources for learning functional programming.
Code examples in this article are in the Haskell programming language. All that you need to understand for this article is the basic function syntax:
even :: Int -> Bool even = . -- implementation goes here
This defines a one-argument function named even. The first line is the type declaration, which says that even takes an Int and returns a Bool. The implementation follows and consists of one or more equations. We'll ignore the implementation (the name and type tell us enough):
map :: (a -> b) -> [a] -> [b] map = .
In this example, map is a function that takes two arguments:
and returns a list of b. Again, we don't care about the definition—the type is more interesting! a and b are type variables that could stand for any type. In the expression below, a is Int and b is Bool:
map even [1,2,3]
It evaluates to a [Bool]:
[False,True,False]
If you see other syntax that you do not understand, don't panic; full comprehension of the syntax is not essential.
Programming and development
Let's begin by dispelling common misconceptions:
At its core, functional programming is just programming with functions—pure mathematical functions. The result of a function depends only on the arguments, and there are no side effects, such as I/O or mutation of state. Programs are built by combining functions together. One way of combining functions is function composition:
(.) :: (b -> c) -> (a -> b) -> (a -> c) (g . f) x = g (f x)
This infix function combines two functions into one, applying g to the output of f. We'll see it used in an upcoming example. For comparison, the same function in Python looks like:
def compose(g, f): return lambda x: g(f(x))
The beauty of functional programming is that because functions are deterministic and have no side effects, you can always replace a function application with the result of the application. This substitution of equals for equals enables equational reasoning. Every programmer has to reason about their code and others', and equational reasoning is a great tool for doing that. Let's look at an example. You encounter the expression:
map even . map (+1)
What does this program do? Can it be simplified? Equational reasoning lets you analyze the code through a series of substitutions:
map even . map (+1) map (even . (+1)) -- from definition of 'map' map (\x -> even (x + 1)) -- lambda abstraction map odd -- from definition of 'even'
We can use equational reasoning to understand programs and optimize for readability. The Haskell compiler uses equational reasoning to perform many kinds of program optimizations. Without pure functions, equational reasoning either isn't possible, or requires an inordinate effort from the programmer.
What do you need from a programming language to be able to do functional programming?
Doing functional programming meaningfully in a language without higher-order functions (the ability to pass functions as arguments and return functions), lambdas (anonymous functions), and generics is difficult. Most modern languages have these, but there are differences in how well different languages support functional programming. The languages with the best support are called functional programming languages. These include Haskell, OCaml, F#, and Scala, which are statically typed, and the dynamically typed Erlang and Clojure.
Even among functional languages there are big differences in how far you can exploit functional programming. Having a type system helps a lot, especially if it supports type inference (so you don't always have to type the types). There isn't room in this article to go into detail, but suffice it to say, not all type systems are created equal.
As with all languages, different functional languages emphasize different concepts, techniques, or use cases. When choosing a language, considering how well it supports functional programming and whether it fits your use case is important. If you're stuck using some non-FP language, you will still benefit from applying functional programming to the extent the language supports it.
Recall that the result of a function depends only on its inputs. Alas, almost all programming languages have "features" that break this assumption. Null values, type case (instanceof), type casting, exceptions, side-effects, and the possibility of infinite recursion are trap doors that break equational reasoning and impair a programmer's ability to reason about the behavior or correctness of a program. (Total languages, which do not have any trap doors, include Agda, Idris, and Coq.)
Fortunately, as programmers, we can choose to avoid these traps, and if we are disciplined, we can pretend that the trap doors do not exist. This idea is called fast and loose reasoning. It costs nothing—almost any program can be written without using the trap doors—and by avoiding them you win back equational reasoning, composability and reuse.
Let's discuss exceptions in detail. This trap door breaks equational reasoning because the possibility of abnormal termination is not reflected in the type. (Count yourself lucky if the documentation even mentions the exceptions that could be thrown.) But there is no reason why we can't have a return type that encompasses all the failure modes.
Avoiding trap doors is an area in which language features can make a big difference. For avoiding exceptions, algebraic data types can be used to model error conditions, like so:
-- new data type for results of computations that can fail -- data Result e a = Error e | Success a -- new data type for three kinds of arithmetic errors -- data ArithError = DivByZero | Overflow | Underflow -- integer division, accounting for divide-by-zero -- safeDiv :: Int -> Int -> Result ArithError Int safeDiv x y = if y == 0 then Error DivByZero else Success (div x y)
The trade-off in this example is that you must now work with values of type Result ArithError Int instead of plain old Int, but there are abstractions for dealing with this. You no longer need to handle exceptions and can use fast and loose reasoning, so overall it's a win.
Most modern statically typed languages have generics (also called parametric polymorphism), where functions are defined over one or more abstract types. For example, consider a function over lists:
f :: [a] -> [a] f = .
The same function in Java looks like:
static List f(List xs)
The compiled program is a proof that this function will work with any choice for the type a. With that in mind, and employing fast and loose reasoning, can you work out what the function does? Does knowing the type help?
In this case, the type doesn't tell us exactly what the function does (it could reverse the list, drop the first element, or many other things), but it does tell us a lot. Just from the type, we can derive theorems about the function:
Theorem 1 helps us understand what the code is doing, and Theorem 2 is useful for program optimization. We learned all this just from the type! This result—the ability to derive useful theorems from types—is called parametricity. It follows that a type is a partial (sometimes complete) specification of a function's behavior, and a kind of machine-checked documentation.
Now it's your turn to exploit parametricity. What can you conclude from the types of map and (.), or the following functions?
Perhaps you have been convinced that functional programming is a better way to write software, and you are wondering how to get started? There are several approaches to learning functional programming; here are some I recommend (with, I admit, a strong bias toward Haskell):
In this article, I discussed what functional programming is and is not, and looked at advantages of functional programming, including equational reasoning and parametricity. We learned that you can do some functional programming in most programming languages, but the choice of language affects how much you can benefit, with functional programming languages, such as Haskell, having the most to offer. I also recommended resources for learning functional programming.
Functional programming is a rich field and there are many deeper (and denser) topics awaiting exploration. I would be remiss not to mention a few that have practical implications, such as:
I hope that you have enjoyed this introduction to functional programming and are inspired to dive into this fun and practical approach to software development.
This article is published under the CC BY 4.0 license.