Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Functional Programming in Go

You're reading from   Functional Programming in Go Apply functional techniques in Golang to improve the testability, readability, and security of your code

Arrow left icon
Product type Paperback
Published in Mar 2023
Publisher Packt
ISBN-13 9781801811163
Length 248 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Dylan Meeus Dylan Meeus
Author Profile Icon Dylan Meeus
Dylan Meeus
Arrow right icon
View More author details
Toc

Table of Contents (17) Chapters Close

Preface 1. Part 1: Functional Programming Paradigm Essentials
2. Chapter 1: Introducing Functional Programming FREE CHAPTER 3. Chapter 2: Treating Functions as First-Class Citizens 4. Chapter 3: Higher-Order Functions 5. Chapter 4: Writing Testable Code with Pure Functions 6. Chapter 5: Immutability 7. Part 2: Using Functional Programming Techniques
8. Chapter 6: Three Common Categories of Functions 9. Chapter 7: Recursion 10. Chapter 8: Readable Function Composition with Fluent Programming 11. Part 3: Design Patterns and Functional Programming Libraries
12. Chapter 9: Functional Design Patterns 13. Chapter 10: Concurrency and Functional Programming 14. Chapter 11: Functional Programming Libraries 15. Index 16. Other Books You May Enjoy

What is functional programming?

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)

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.

You have been reading a chapter from
Functional Programming in Go
Published in: Mar 2023
Publisher: Packt
ISBN-13: 9781801811163
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image