Book Image

Learning Functional Programming in Go

By : Lex Sheehan
Book Image

Learning Functional Programming in Go

By: Lex Sheehan

Overview of this book

Lex Sheehan begins slowly, using easy-to-understand illustrations and working Go code to teach core functional programming (FP) principles such as referential transparency, laziness, recursion, currying, and chaining continuations. This book is a tutorial for programmers looking to learn FP and apply it to write better code. Lex guides readers from basic techniques to advanced topics in a logical, concise, and clear progression. The book is divided into four modules. The first module explains the functional style of programming: pure functional programming, manipulating collections, and using higher-order functions. In the second module, you will learn design patterns that you can use to build FP-style applications. In the next module, you will learn FP techniques that you can use to improve your API signatures, increase performance, and build better cloud-native applications. The last module covers Category Theory, Functors, Monoids, Monads, Type classes and Generics. By the end of the book, you will be adept at building applications the FP way.
Table of Contents (21 chapters)
Title Page
Credits
About the Author
Acknowledgments
About the Reviewer
www.PacktPub.com
Customer Feedback
Preface
Index

Fibonacci sequence - a simple recursion and two performance improvements


The Fibonacci sequence is a sequence of numbers where each number is equal to the previous two numbers added together. Here's an example of this:

 1  1  2  3  5  8  13  21  34

So, 1 plus 1 is 2, 2 plus 3 is 5, 5 plus 8 is 13, and so on.

Let's use the Fibonacci sequence to help illustrate a number of concepts.

A recursive function is a function that calls itself in order to break down complex input into simpler ones. With each recursive call, the input problem must be simplified in such a way that eventually the base case must be reached.

The Fibonacci sequence can be easily implemented as a recursive function:

func Fibonacci(x int) int {
if x == 0 {
return 0
} else if x <= 2 {
return 1
} else {
return Fibonacci(x-2) + Fibonacci(x-1)
    }
}

In the preceding recursive function (Fibonacci), if the input is the simple case of 0 then it returns 0. Similarly, if the input is 1 or 2 then return 1.

An input of 0, 1 or 2 is called the base case or stopping condition; else, fib will call itself twice, adding the previous value in the sequence to the one preceding it:

Fibonacci(5) calculation graph

In the preceding figure Fibonacci(5) calculation graph, we can visually see how the fifth element in the Fibonacci sequence is calculated. We see f(3) is calculated twice and f(2) is calculated thrice. Only the final leaf nodes of 1 are added together to calculate the sum total of 5:

func main() {
   fib := Fibonacci
   fmt.Printf("%vn", fib(5))
}

Run that code and you'll get 5. Recursive functions perform identical calculations over and over again; f(3) is calculated twice and f(2) is calculated thrice. The deeper the graph, the more redundant calculations get executed. That is terribly inefficient. Try it yourself. Pass a value greater than 50 to fib and see how long you have to wait for the final result.

Go provides many ways to improve this performance. We'll look at two options: memoization and concurrency.

Memoization is an optimization technique used to increase performance by storing the results of expensive function calls and returning the cached result when the same input occurs again.

Memoization works well because of the following two properties of pure functions:

  • They always return the same result given the same input(s)
  • They have no side effects in the environment in which they run

Memoization

Let's utilize a memoization technique to speed up our Fibonacci calculation.

First, let's create a function type named Memoized() and define our Fibonacci variable to be of that type:

type Memoized func(int) int
var fibMem Memoized

Next, let's implement theMemoize()function. The key thing to realize here is that as soon as our application starts, even before ourmain()function is executed, ourfibMemvariable get wired up. If we were to step through our code we'd see that ourMemoizefunction is called. The cache variable is assigned and our anonymous function is returned and assigned to our fibMem function literal variable.

funcMemoize(mf Memoized) Memoized {
       cache := make(map[int]int)
return func(key int) int {
if val, found := cache[key]; found {
return val
              }
              temp := mf(key)
              cache[key] = temp
return temp
       }
}

Memoize takes a Memoized() function type as its input and returns a Memoized() function.

In the first line of Memoize, we create a variable of the type map to act as our cache in order to hold computed Fibonacci computations.

Next, we create a closure that is of the type Memoized(), which is returned by the Memoize() function. Note that a closure is an inner function that closes over or that has access to variables in its outer scope.

Inside the closure, if we find the computation for the passed integer, we return its value from the cache; else we call the recursive Fibonacci function(mf) with the integer parameter (key), whose return value will be stored incache[key]. Next time, when the same key is requested its value will be returned directly from the cache.

An anonymous function is a function defined with no name. When an anonymous function includes logic that can access variables defined in its scope, for example, cache, and if that anonymous function can be passed as an argument or returned as the value of function calls, which is true in this case, then we can refer to this anonymous function as a lambda expression.

We'll implement the logic of the Fibonacci Sequence in a function named fib:

func fib(x int) int {
if x == 0 {
return 0
} else if x <= 2 {
return 1
} else {
return fib(x-2) + fib(x-1)
   }
}

The last thing we do in our memoize.go file is to create the following function:

func FibMemoized(n int) int {
return fibMem(n)
}

Now, it's time to see if our wiring works properly. In our main() function when we execute our println statement, we get the correct output.

println(fibonacci.FibMemoized(5))

The following is the output:

5

We can verify that 5 is the correct answer by glancing back at our Fibonacci(5)calculation graph shown earlier in this chapter.

If we were to step through our code using a debugger, we'd see that fibonacci.FibMemoized(5) calls the following

func FibMemoized(n int) int {
return fibMem(n)
}

And the value of n variable is 5. Since fibMem is pre-wired, we start executing at the return statement (and we have access to the cache variable that has already been initialized) . So, we begin executing at the return statement shown in the following code (from the Memoize function):

return func(key int) int {
if val, found := cache[key]; found {
return val
   }
   temp := mf(key)
   cache[key] = temp
return temp
}

Since this is the first time through, there are no entries in the cache and we skip past the body of the if block and run temp := mf(key)

That calls the fib function:

func fib(x int) int {
if x == 0 {
return 0
} else if x <= 2 {
return 1
} else {
return fib(x-2) + fib(x-1)
   }
}

Run the fib function as follows:

Since x is greater than 2 we run the last else statement that recursively calls fib twice. Recursive calls to fib continues until the base conditions are reached and the final result is calculated and returned.