A few weeks ago I was preparing a small introduction to functional programming. It turns out, for me at least, to be fairly difficult to define what functional programming is. I distilled it down to 3 things via process of elimination. In this post I dive into what these 3 things are and what benefits they bring.
Sidebar: As someone who has been doing OOP for 15+ years at this stage, I find OOP difficult to define too. This was not always the case. Learning functional programming ruined me, as it has ruined many before. As an OO programmer I was sure of my knowledge, my patterns, my design. Then I tried learning something that seemed to turn it all on it's head. Not just my knowledge but my self-assuredness in "right" and "wrong" ways to build software. Now with a little more experience in FP, I see many similarities in the problems and how they are solved. For me the benefit in FP is the number of "patterns and practices" that need to be understood to write better software. The point of this sidebar though is that words like abstraction and encapsulation are not claimed exclusively by OO. Except for inheritance... OO can have that if it wants it!
In the following sections I discuss my 3 aspects of programming that should be followed to reap the benefits of FP.
- The language should support higher-order functions
- More complex functions should be composed out of simpler functions
- The programmer should follow the discipline of making a distinction between pure and impure functions and try maximize the amount of pure functions
I find it interesting that there are 3 moving parts here. The language, how we build code, and how we architect code to interact with the world.
A higher-order function is a function that meets at least one of the following two criteria:
- A function that takes another function as an input
- A function that returns a function as its output
Although most modern languages support this now days, functional-first languages tend to make this feel a lot more natural to use.
let isEven x = (x % 2) = 0 // predicate function for determining an even number let selectEven = List.filter isEven // Use predicate to returns new function of that selects even numbers let evenInts = selectEven [0..10] // use the function
- Functions are values and so can be passed around
- Functions that take functions can be far more flexible as behaviour can be decided by the caller
- When returning a function from another function it can be evaluated later (or not at all)
Function composition is the combination of simple functions into more complex ones. To compose functions, the output type of a function needs match the input type for the next function in the composition.
This is probably easiest explained with examples since you have probably used it in both school mathematics and programming.
Say we want to normalize some strings by trimming the whitespace off and making them lower-case. you could do this like this.
let trim (s : string) = s.Trim() let lower (s : string) = s.ToLowerInvariant() let normalize (s : string) = lower(trim(s))
This is composition. In a functional-first language like F# we can build this up in a way that structurally matches the order the functions are called.
let normalize = trim >> lower
This creates a new function
normalize out of the 2 functions
lower. Remember that the output type of
trim needs to match the input type of
lower. In this case they are both
- Functions are small testable units
- Small and generic functions enable reusability
- We build more and more complex functions out of simpler functions helps in building in small steps
Maximize use of pure functions
This one is fairly uncontroversial. If you have heard people talking about FP then you have likely heard about pure functions.
At this point talk of referential transparency comes up. Referential transparency seems to be a term borrowed from analytical philosophy. If something is referentially transparent it means it's value is not dependent on some context, like the time it is referenced. From a code perspective, this means that once something is assigned a value, that value does not change over the lifetime of the programs execution. Put more flippantly, "equals equals equals".
Ok. Cool, cool, cool. What does this mean?
At this point I need to issue a disclaimer: I am not a computer scientist. This is just my understanding on a topic where people tend to throw around terms like it is some kind of intellectual contest.
The characteristic people are more often seeking with pure functions is side-effect free functions. Side-effect free is much easier to understand than referential transparency. It means that nothing outside the scope of the function is mutated.
So for a function to be pure, it needs to satisfy 2 criteria:
- The function must be referentially transparent
- The function must be side effect free
Note that I said maximize pure functions. We cannot build programs that interact with the outside world without having side effects. What we strive for in FP is increasing the amount of functions that are pure and pushing the side-effects to the boundary of our applications. We will dive into this in another post when discussing architecture.
// Referentially transparent: Yes // Side-effect free: No // Pure: No let i_am_rt x = printfn "I am referentially transparent." x // Referentially transparent: No // Side-effect free: No // Pure: No let i_am_not_rt x = (System.Console.ReadLine() |> int) + x // Referentially transparent: Yes // Side-effect free: Yes // Pure: Yes let i_am_pure x = x + 1
In practical terms, honouring referential transparency means you are not reading any data that is not in the input. Side-effect free is ensuring you are not changing input values (by reference) or mutating any state in the program or outside systems. It is interesting to note that immutability comes along for the ride with pure functions, at least for where it really matters when programming with immutable values.
- Calls to a function are idempotent, so they can be repeated without fear of unexpected state updates
- If a function does not depend on the output of another function, the order does not matter
- Since pure functions depend only on their input, they can be called in parallel without fear of deadlock or data corruption
- Pure functions are easy to test because they depend on only the input and must have an output (to be useful)
- Since a pure function only depends on input, reasoning about it should be simpler
- If the value of a pure function is not used, it can be removed without altering the behaviour of a program
I shouldn't have to say it, but I will. Functional programming is not about AVOIDING mutating state. It is not BAD at mutating state. It is just more opinionated about WHERE those mutations occur.
In this post we looked at some of the core ideas of functional programming.
My opinion then is that for code to claim to be functional:
- The language it is written in should support higher-order functions
- More complex code should be built from composing simpler functions together
- The programmer should follow the discipline of keeping functions pure as much as possible and push impure functions to the boundaries of the application
These ideas all have a long tradition in mathematics but hopefully from the benefits listed you can see that they might have some real practical benefits if adopted into how you design and implement applications. Depending on what languages you have been exposed to, you may have expected other topics here like immutability and algebraic data types. These language constructs being built into the language can really help, but I don't believe are necessary for programming in a functional way. In the next post we will look at these to see what benefits they bring.