Function-Oriented-Programming (FOP)
If you’re familiar with Object-Oriented-Programming and you’ve heard of Functional Programming (FP) but struggled to understand it or had difficulty seeing the benefits, this article is for you. In this article, I will discuss some core concepts of FP and how it is simply Function-Oriented-Programming (FOP).
Functions are everything in FP, and there are many different types of functions. In this article I will discuss three types of functions:
- Pure Functions
- Honest Functions
- Higher Order Functions
Pure Functions
The english definition of pure is:
not mixed or adulterated with any other substance or material
Some synonyms for the word pure are:
- Unmixed
- Uncontaminated
So what are we mixing or contaminating functions with that can make them impure?
To figure that out, let’s go back to Year 10 mathematics and revisit what a function is.
Hopefully we all remember the linear function with the formula y = mx + c
:
So this function takes a number as a parameter and returns a number.
If we wanted to write this function in TypeScript it would be:
function y(x: number): number {
return (2 * x) + 1
}
We know that if we pass 2
to this function we will get 5
. It doesn’t matter how many times we call it, what time of the day it is, what the weather outside is, it will always return 5
.
That is one key element of a pure function, its return value is solely based on the parameters passed in.
The other aspect is that it has no side effect. A side effect is something that changes the state of the world.
The world is anything outside this function:
- Shared mutable state
- I/O operations
Some rules of thumb regarding impure functions:
- If it takes no parameters, it’s probably impure as it will return different results, otherwise, if it returns the same value every time, that is called a constant.
An example of this isMath.random()
. Every time you call it, it returns something different.
- If it doesn’t return anything, it’s probably impure as it will have a side effect, otherwise, there’s no reason to call it.
An example of this isconsole.log("SOMETHING")
. The value it returns isundefined
but it has the side effect of having something printed to the console.
- If it returns a Promise, it’s probably impure as most asynchronous tasks are usually an I/O operation.
An example of this isfetch("https://google.com")
. It returns a Promise as it needs to send a request and wait for the response, this is an I/O operation and is thus impure.
Now that we have a better understanding of what a pure function is, what’s so great about them?
- Easier to reason about
A function that deals with I/O and/or shared mutable state has a lot of moving parts. On the other hand, as a pure function’s return value is solely based on it’s input parameters and it doesn’t have any side effects, you have less things to keep in your head. - Can safely be executed concurrently
The problem with concurrency is side effects. If the function has side effects, running it concurrently can have some unwanted and unexpected results, even with good concurrency code around it. Pure functions remove this complexity all together. - Testing them is simple
These functions are fun to test as you can simply iterate through a list of inputs and expected outputs and not have to worry about mocking behaviours.
So, now you might think pure functions are great but my program needs to do I/O operations like getting and writing to a DB. You are correct, a program with only pure functions is absolutely useless.
The thing to strive for is to separate the pure from the impure as much as you can and push impurity to the boundaries.
Honest Functions
The english definition of honest is:
free of deceit, truthful and sincere
At a basic level, these are the features of a function:
- Name
- Parameters
- Return Type
- Body
We are taught early on in our programming career that a consumer shouldn’t have to look at the body of the function to understand what it does. Primarily, we are taught that naming a function correctly is extremely important for this exact reason. Whilst this is still true, when it comes to functional programming, the features of a function that are the most important from an honesty point of view are the parameters and the return type.
In math, these are called the domain and codomain.
Lets take the following function:
function isEven(num: number): boolean {
return num % 2 === 0
}
The domain of this function is all real numbers and the codomain is the binary result of true or false.
To explain how parameters or return types can be deceitful, I am going to write a simple function that takes the age of a person and return the stage of their life (source).
function getStageOfLife(age: number): string {
if(age < 0) {
throw new Error("INVALID AGE")
}
if(age <= 1) {
return "Infant"
}
if(age <= 4) {
return "Toddler"
}
if(age <= 12) {
return "Child"
}
if(age <= 19) {
return "Teen"
}
if(age <= 39) {
return "Adult"
}
if(age <= 59) {
return "Middle Age Adult"
}
return "Senior Adult"
}
This function is not honest. First and foremost, the co-domain (return type) is too large. The caller of the function doesn’t know what to expect as the result without looking at the body of the function. Let’s fix this by restricting the co-domain by adding an enum for the different stages of life:
enum StageOfLife {
Infant,
Toddler,
Child,
Teen,
Adult,
MiddleAgeAdult,
SeniorAdult
}
Now the function becomes:
function getStageOfLife(age: number): StageOfLife {
if(age < 0) {
throw new Error("INVALID AGE")
}
if(age <= 1) {
return StageOfLife.Infant
}
if(age <= 4) {
return StageOfLife.Toddler
}
if(age <= 12) {
return StageOfLife.Child
}
if(age <= 19) {
return StageOfLife.Teen
}
if(age <= 39) {
return StageOfLife.Adult
}
if(age <= 59) {
return StageOfLife.MiddleAgeAdult
}
return StageOfLife.SeniorAdult
}
This is already much more honest, but there is still an issue: If the age is less than 0, the program throws an error. The only way a caller would know this is by looking at the body of the function, but the goal is to convey as much information as we can through the domain and co-domain.
There are two approaches to addressing this problem:
- Restrict the domain
Instead of taking anumber
as an input, introduce a new Tiny Type (using a library like tiny-types) calledAge
which encapsulates all the rules of what a validAge
is. The consumer will then need to construct anAge
before they can call this function.
function getStageOfLife({ value: age }: Age): StageOfLife {
if(age <= 1) {
return StageOfLife.Infant
}
if(age <= 4) {
return StageOfLife.Toddler
}
if(age <= 12) {
return StageOfLife.Child
}
if(age <= 19) {
return StageOfLife.Teen
}
if(age <= 39) {
return StageOfLife.Adult
}
if(age <= 59) {
return StageOfLife.MiddleAgeAdult
}
return StageOfLife.SeniorAdult
}
- Expand the co-domain
Instead of throwing an error, you make the function returnStageOfLife | undefined
to cater for the invalid age. That way the caller is forced to handle theundefined
path.
function getStageOfLife(age: number): StageOfLife | undefined {
if(age < 0) {
return undefined
}
if(age <= 1) {
return StageOfLife.Infant
}
if(age <= 4) {
return StageOfLife.Toddler
}
if(age <= 12) {
return StageOfLife.Child
}
if(age <= 19) {
return StageOfLife.Teen
}
if(age <= 39) {
return StageOfLife.Adult
}
if(age <= 59) {
return StageOfLife.MiddleAgeAdult
}
return StageOfLife.SeniorAdult
}
Implementing either of these two options will make the function as honest as possible. The consumer of this function will be forced to handle all possible results without having to look at the body of the function.
Higher Order Functions
A Higher Order Function (HOF) is a function that either takes a function as an argument and/or returns a function.
This is a very powerful feature that enables us to be much more declarative in our programs.
Declarative code tells the program what to do, as opposed to imperative code that tells the program how to do it.
Imperative code is written with the use of statements, whilst declarative code uses only expressions.
An expression is logic that returns a value. A statement, is an operation or a command. Control flow operations like if/else, for/while loops are all statements. They tell the program how to do things.
When the code is telling the program how to do things, this is called imperative code. On the other hand, declarative code tells the program what to do.
One key goal of FP is to write declarative code and it does so by minimising the use of statements. One way this is achieved is by hiding away the statements in a HOF.
I will explain this further with an example using the following array of fruits:
interface Fruit {
name: string;
color: string;
}const fruits: Fruit[] = [
{
name: "Apple",
color: "red"
},
{
name: "Banana",
color: "yellow"
},
{
name: "Lemon",
color: "yellow"
}
]
Now if we wanted to get the names of all yellow fruits as an array we could write the following imperative code:
const names: string[] = []for(let fruit of fruits) {
if(fruit.color === "yellow") {
names.push(fruit.name)
}
}
This code is imperative as it is using the for/of
statement and if
statement to tell the program how to build up the array of names.
This is the equivalent declarative code:
const names = fruits
.filter(fruit => fruit.color === "yellow")
.map(fruit => fruit.name)
This code is declarative as it is only using expressions and it tells the program what to do instead of how to do it.
Not only is this less lines of code, but it is also easier to read as you can almost read it out in english: Take the fruits array, filter to only the yellow fruits and then map over them to get the name.
This is only possible due to the filter
and map
HOF. As you can see they are each taking a function as a parameter, making them a HOF.
Another type of HOF is one that returns a function. I will create a HOF that we can call to get the function to pass to the filter above.
function fruitIsColor(color: string): (fruit: Fruit) => boolean {
return fruit => fruit.color === color
}
Now we can change the above code to:
const names = fruits
.filter(fruitIsColor("yellow"))
.map(fruit => fruit.name)
Conclusion
In this article I have tried to demystify Functional Programming by calling it a name that we might be more comfortable with: Function-Oriented-Programming.
I have explained a few different properties of a function and why they are important.
As functions are a programmer’s bread and butter, I have tried to illustrate how we can use FP principles in our own functions to improve the code that we write everyday.
These principles are:
- Separating pure functions from the impure ones as much as we can, and to try and keep the impurity at the boundaries of our systems
- Write honest functions through the input/output parameter types to ensure the type system assists the consumer as much as possible
- Use higher order functions to extract the how, so our code can focus on the what.