In my previous post, Building Everything with a Screwdriver, I outlined the challenges that are faced when switching from procedural to functional programming. In part 2 of my functional programming blog series, I would like to describe the next challenge faced, terminology. Take a look at the following diagram of functional programming concepts from the Fantasy Land specification. Gibberish, right?
To underscore how challenging it is to explain functional programming terminology, I would like to quote James Iry, who wrote in the “Brief, Incomplete and Mostly Wrong History of Programming Languages“:
While engaged in a conversation with some colleagues, I attempted to explain the State and Freer Monads. My explanation was, “A Monad is just a data structure that’s flat mappable!” As the words began to leave my mouth and enter my ears, I realized that my explanation sounded like gibberish. I could tell that my colleagues were disappointed with my explanation; I could now see why some programmers consider functional programmers as “elitist.” I decided to consult with other colleagues, and after a constructive discussion, we came to this answer:
All the fancy terms used by functional programmers are just names for standardized interfaces that describe the laws that the implementation must abide by.
Similarly, terms like Singleton, Factory, Twin, Mediator, and Memento, are just names of patterns in Object-Oriented programming. To the layman, these terms also hold a sense of magic. However, the key difference between functional programming and object-orient programming is that these interfaces also describe the properties of the implementation that must be true.
Requiring that an interface be universally true means that by applying functional programming, large parts of the code are naturally reusable and, in some cases, edge cases are reduced because of the laws the pattern applies. So, what are these laws? Let’s take the three laws of a Monad as an example.
The Left Identity
For all intended purposes, a Monad is just a box with special rules. The first rule is that given a value in a box, i.e. a monad, when a function is applied that returns the same type of box, i.e. a box of boxes, it can be flattened into a single box. As scary as this sounds, this is something most developers have seen. Consider the following example:
List<Integer> list1 = Arrays.asList(1,2,3); List<Integer> list2 = Arrays.asList(4,5,6); List<Integer> list3 = Arrays.asList(7,8,9); List<List<Integer>> listOfLists = Arrays.asList(list1, list2, list3); List<Integer> listOfAllIntegers = listOfLists.stream() .flatMap(x -> x.stream()) .collect(Collectors.toList()); assertEquals(listOfAllIntegers, Arrays.asList(1,2,3,4,5,6,7,8,9));
Here we can see that if we apply flatMap to a list of lists, we get back a list. This is the first law of a Monad…but wait! Am I suggesting that a List is a Monad? Yes, yes, it is! Not so scary, right? You’ve probably been using Monads all along; it’s just learning the name. A list of lists of any type, when flattened, will produce a list of all those values.
The Right Identity
The second law of Monads is that if you have a value in a box, then any function that re-wraps that value in the same box, can be flattened and it must not change the value. Consider the following example:
Future<String> one = CompletableFuture.completedFuture("Hello"); Future<String> two = one .thenCompose(CompletableFuture::completedFuture); assertEquals(one.get(), two.get());
The last law of Monads is associativity. In summary, it means that if you apply “The Left Identity” and “The Right Identity” to a Monad, regardless of the order, it should always produce the same result. Consider the following example:
Optional<Integer> one = Optional.of(10) .flatMap(value = > Optional.of(value + 5)) .flatMap(Optional::of); Optional<Integer> two = Optional.of(10) .flatMap(Optional::of) .flatMap(value => Optional.of(value + 5)); AssertEquals(one.get(), two.get()); Here we see that regardless of the order of operations of flatMap on a Monad, the value must be the same. We can also see here that Optional is a Monad.
The interesting aspect of these laws is that, given any type that satisfies these laws, it is considered a Monad. But what does this buy us? Well, the power is that any algebraic function that can be applied to Monads will now operate on your Monadic Type. Since these algebraic functions adhere to the laws of mathematics, they are inherently easy to test and reuse.
The overall terminology of functional programming may seem daunting and confusing, but when embraced, it can bring a lot of power. By using these patterns, code becomes much easier to reason, test, and reuse because of the mathematics underlying the code. What’s nice about it is that you’re probably already doing some form of functional programming, and all it takes is just understanding the names applied to the patterns. Hopefully, this has sparked some interest in learning functional programming. Please check back for part 3 of this series, where we will be looking at some stateful procedural code and refactoring it to stateless functional code.