As you might have guessed, FP is a programming paradigm where functions play the main role. Functions will be the bread and butter of the functional programmer’s toolbox. Our programs will be composed of functions, chained together in various ways to perform ever more complex tasks. These functions tend to be small and modular.
This is in contrast with OOP, where objects play the main role. Functions are also used in OOP, but their use is usually to change the state of an object. They are typically tied to an object as well. This gives the familiar call pattern of someObject.doSomething(). Functions in these languages are treated as secondary citizens; they are used to serve an object’s functionality rather than being used for the function itself.
Introducing first-class functions
In FP, functions are considered first-class citizens. This means they are treated in a similar way to how objects are treated in a traditional object-oriented language. Functions can be bound to variable names, they can be passed to other functions, or even served as the return value of a function. In essence, functions are treated as any other “type” would be. This equivalence between types and functions is where the power of FP stems from. As we will see in later chapters, treating functions as first-class citizens opens a wide door of possibilities for how to structure programs.
Let’s take a look at an example of treating functions as first-class citizens. Don’t worry if what’s happening here is not entirely clear yet; we’ll have a full chapter dedicated to this later in the book:
package main
import “fmt”
type predicate func(int) bool
func main() {
is := []int{1, 1, 2, 3, 5, 8, 13}
larger := filter(is, largerThan5)
fmt.Printf(“%v”, larger)
}
func filter(is []int, condition predicate) []int {
out := []int{}
for _, i := range is {
if condition(i) {
out = append(out, i)
}
}
return out
}
func largerThan5(i int) bool {
return i > 5
}
Let’s break what’s happening here down a bit. First, we are using a “type alias” to define a new type. The new type is actually a “function” and not a primitive or a struct:
type predicate func(int) bool
This tells us that everywhere in our code base where we find the predicate
type, it expects to see a function that takes an int
and returns a bool
. In our filter
function, we are using this to say we expect a slice of integers as input, as well as a function that matches the predicate
type:
func filter(is []int, condition predicate) []int {…}
These are two examples of how functions are treated differently in functional languages from in object-oriented languages. First, types can be defined as functions instead of just classes or primitives. Second, we can pass any function that satisfies our type signature to the filter
function.
In the main
function, we are showing an example of passing the isLargerThan5
function to the filter
function, similar to how you’d pass around objects in an object-oriented language:
larger := filter(is, largerThan5)
This is a small example of what we can do with FP. This basic idea, of treating functions as just another type in our system that can be used in the same way as a struct, will lead to the powerful techniques that we explore in this book.
What are pure functions?
FP is often thought of as a purely academic paradigm, with little to no application in industry. This, I think, stems from an idea that FP is somehow more complicated and less mature for the industry than OOP. While the roots of FP are academic, the concepts that are central to these languages can be applied to many problems that we solve in industry.
Often, FP is thought of as more complex than traditional OOP. I believe this is a misconception. Often, when people say FP, what they really mean to say is pure FP. A pure functional program is a subset of FP, where each function has to be pure – it cannot mutate the state of a system or produce any side effects. Hence, a pure function is completely predictable. Given the same set of inputs, it will always produce the same set of outputs. Our program becomes entirely deterministic.
This book will focus on FP without treating it as the stricter subset of “pure” FP. That is not to say that purity brings us no value. In a purely functional language, functions are entirely deterministic and the state of a system is unchanged by calling them. This makes code easier to debug and comprehend and improves testability. Chapter 6 is dedicated to function purity, as it can bring immense value to our programs. However, eradicating all side effects from our code base is often more trouble than it’s worth. The goal of this book is to help you write code in a way that improves readability, and as such, we’ll often have to make a trade-off between the (pure) functional style and a more forgiving style of FP.
To briefly and rather abstractly show what function purity is, consider the following example. Say we have a struct of the Person
type, with a Name
field. We can create a function to change the name of the person, such as changeName
. There are two ways to implement this:
- We can create a function that takes in the object, changes the content of the
name
field to the new name, and returns nothing.
- We can create a function that takes in an object and returns a new object with the changes applied. The original object is not changed.
The first way does not create a pure function, as it has changed the state of our system. If we want to avoid this, we can instead create a changeName
function that returns a new Person
object that has identical field values for each field as the original Person
object does, but instead has a new name in the name
field. The diagram here shows this a bit more abstractly:
Figure 1.1: Pure function (top) compared to impure function (bottom)
In the top diagram, we have a function (denoted with the Lambda symbol) that takes a certain object, A, as input. It performs an operation on this object, but instead of changing the object, it returns a new object, B, which has the transformation applied to it. The bottom diagram shows what was explained in the earlier paragraph. The function takes object A, makes a change “in-place” on the object’s values, and returns nothing. It has only changed the state of the system.
Let’s take a look at what this would look like in code. We start off by defining our struct, Person
:
type Person struct {
Age int
Name string
}
To implement the function that mutates the Person
object and places a new value in the Name
field, we can write the following:
func changeName(p *Person, newName string) {
p.Name = newName
}
This is equivalent to the bottom of the diagram; the Person
object that was passed to the function is mutated. The state of our system is now different from before the function was called. Every place that refers to that Person
object will now see the new name instead of the old name.
If we were to write this in a pure function, we’d get the following:
func changeNamePure(p Person, newName string) Person {
return Person{
Age: p.Age,
Name: newName,
}
}
In this second function, we copy over the Age
value from the original Person
object (p
) and place the newName
value in the Name
field. The result of this is returned as a new object.
While it’s true that the former, impure way of writing code seems easier superficially and takes less effort, the implications for maintaining a system where functions can change the state of the system are vast. In larger applications, maintaining a clear understanding of the state of your system will help you debug and replicate errors more easily.
This example looks at pure functions in the context of immutable data structures. A pure function will not mutate the state of our system and always return the same output given the same input.
In this book, we will focus on the essence of FP and how we can apply the techniques in Go to create more readable, maintainable, and testable code. We will look at the core building blocks, such as higher-order functions, function currying, recursion, and declarative programming. As mentioned previously, FP is not equivalent to “pure” FP, but we will discuss the purity aspect as well.
Say what you want, not how you want it
One commonality that is shared between FP languages is that functions are declarative rather than imperative. In a functional language, you, as the programmer, say what you want to achieve rather than how to achieve it. Compare these two snippets of the Go code.
The first snippet here is an example of valid Go code where the result is obtained declaratively:
func DeclarativeFunction() int {
return IntRange(-10,10).
Abs().
Filter(func(i int64) bool {
return i % 2 == 0
}).
Sum()
// result = 60
}
Notice how, in this code, we say the following things:
- Give us a range of integers, between -10 and 10
- Turn these numbers into their absolute value
- Filter for all the even numbers
- Give us the sum of these even numbers
Nowhere did we say how to achieve these things. In an imperative style, the code would look like the following:
func iterativeFunction() int {
sum := 0
for i := -10; i <= 10; i++ {
absolute := int(math.Abs(float64(i)))
if absolute%2 == 0 {
sum += absolute
}
}
return sum
}
While, in this example, both snippets are easy to read for anyone with some Go experience, we can imagine how this would stop being the case for larger examples. In the imperative example, we have to spell out literally how the computer is supposed to give us a result.