DEV Community

Cover image for Functional programming in Go
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Functional programming in Go

Written by Michiel Mulders✏️

Why would you practice functional programming with Go? To put it simply, functional programming makes your code more readable, easier to test, and less complex due to the absence of states and mutable data. If you encounter bugs, you can debug your app quickly, as long as you don’t violate the rules of functional programming. When functions are isolated, you don’t have to deal with hidden state changes that affect the output.

Software engineer and author Eric Elliot defined function programming as follows.

Functional programming is the process of building software by composing pure functions, avoiding shared state, mutable data, and side-effects. Functional programming is declarative rather than imperative, and application state flows through pure functions. Contrast with object-oriented programming, where application state is usually shared and colocated with methods in objects.

I’ll take it a step further: functional programming, like object-oriented and procedural programming, represents a paradigm shift. It imposes a unique way of thinking when it comes to writing code and introduces a whole new set of rules to stick to.

4 important concepts to understand

To fully grasp functional programming, you must first understand the following related concepts.

  1. Pure functions and idempotence
  2. Side effects
  3. Function composition
  4. Shared state and immutable data

Let’s quickly review.

LogRocket Free Trial Banner

1. Pure functions and idempotence

A pure function always returns the same output if you give it the same input. This property is also referenced as idempotence. Idempotence means that a function should always return the same output, independent of the number of calls.

2. Side effects

Next, a pure function can’t have any side effects. In other words, your function cannot interact with external environments.

For example, functional programming considers an API call to be a side effect. Why? Because an API call is considered an external environment that is not under your direct control. An API can have several inconsistencies, such as a timeout or failure, or it may even return an unexpected value. It does not fit the definition of a pure function since we require consistent results every time we call the API.

Other common side effects include:

  • Data mutation
  • DOM manipulation
  • Requesting conflicting data, such as the current DateTime with time.Now()

3. Function composition

The basic idea of function composition is straightforward: you combine two pure functions to create a new function. This means the concept of producing the same output for the same input still applies here. Therefore, it’s important to create more advanced functionality starting with simple, pure functions.

4. Shared state and immutable data

The goal of functional programming is to create functions that do not hold a state. Shared states, especially, can introduce side effects or mutability problems in your pure functions, rendering them nonpure.

Not all states are bad, however. Sometimes, a state is necessary to solve a certain software problem. The goal of functional programming is to make the state visible and explicit to eliminate any side effects. A program uses immutable data structures to derive new data from using pure functions. This way, there is no need for mutable data that may cause side effects.

Now that we’ve covered our bases, let’s define some rules to follow when writing functional code in Go.

Rules for functional programming

As I mentioned, functional programming is a paradigm. As such, it’s difficult to define exact rules for this style of programming. It’s also not always possible to follow these rules to a T; sometimes, you really need to rely on a function that holds a state.

However, to follow the functional programming paradigm as closely as possible, I suggest sticking to the following guidelines.

  • No mutable data to avoid side effects
  • No state (or implicit state, such as a loop counter)
  • Do not modify variables once they are assigned a value
  • Avoid side effects, such as an API call

One good “side effect” we often encounter in functional programming is strong modularization. Instead of approaching software engineering from the top-down, functional programming encourages a bottom-up style of programming. Start by defining modules that group similar pure functions that you expect to need in the future. Next, start writing those small, stateless, independent functions to create your first modules.

We are essentially creating black boxes. Later on, we’ll tie together the boxes following the bottom-up approach. This enables you to build a strong base of tests, especially unit tests that verify the correctness of your pure functions.

Once you have trust in your solid base of modules, it’s time to tie together the modules. This step in the development process also involves writing integration tests to ensure proper integration of the two components.

5 Functional programming examples in Go

To paint a fuller picture of how functional programming with Go works, let’s explore five basic examples.

1. Updating a string

This is the simplest example of a pure function. Normally, when you want to update a string, you would do the following:


Enter fullscreen mode Exit fullscreen mode

name := "first name"
name := name + " last name"


Enter fullscreen mode Exit fullscreen mode

The above snippet does not adhere to the rules of functional programming because a variable can’t be modified within a function. Therefore, we should rewrite the snippet of code so every value gets its own variable.

The code is much more readable in the snippet below.


Enter fullscreen mode Exit fullscreen mode
firstname := "first"
lastname := "last"
fullname := firstname + " " + lastname
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

When looking at the nonfunctional snippet of code, we have to look through the program to determine the latest state of name to find the resulting value for the name variable. This requires more effort and time to understand what the function is doing.

Next, let’s see how we can avoid updating arrays.

2. Avoid updating arrays

As stated earlier, the objective of functional programming is to use immutable data to derive a new immutable data state through pure functions. This can also be applied to arrays in which we create a new array each time we want to update one.

In nonfunctional programming, update an array like this:


Enter fullscreen mode Exit fullscreen mode

names := [3]string{"Tom", "Ben"}

// Add Lucas to the array
names[2] = "Lucas"
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

Let’s try this according to the functional programming paradigm.


Enter fullscreen mode Exit fullscreen mode
names := []string{"Tom", "Ben"}
allNames := append(names, "Lucas")
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

The example uses the original names slice in combination with the append() function to add extra values to the new array.

3. Avoid updating maps

This is a somewhat more extreme example of functional programming. Imagine we have a map with a key of type string and a value of type integer. The map holds the number of fruits we still have left at home. However, we just bought apples and want to add it to the list.


Enter fullscreen mode Exit fullscreen mode

fruits := map[string]int{"bananas": 11}

// Buy five apples
fruits["apples"] = 5
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

We can accomplish the same functionality under the functional programming paradigm.


Enter fullscreen mode Exit fullscreen mode
fruits := map[string]int{"bananas": 11}
newFruits := map[string]int{"apples": 5}

allFruits := make(map[string]int, len(fruits) + len(newFruits))


for k, v := range fruits {
    allFruits[k] = v
}


for k, v := range newFruits {
    allFruits[k] = v
}
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

Since we don’t want to modify the original maps, the code loops through both maps and adds the values to a new map. This way, data remains immutable.

As you can probably tell by the length of the code, however, the performance of this snippet of is much worse than a simple mutable update of the map because we are looping through both maps. This is the exact point at which you trade better code quality for code performance.

4. Higher-order functions and currying

Most programmers don’t use higher-order functions often in their code, but it comes in handy to establish currying in functional programming.

Let’s assume we have a simple function that adds two integers. Although this is already a pure function, we want to elaborate on the example to showcase how we can create more advanced functionality through currying.

In this case, we can only accept one parameter. Next, the function returns another function as a closure. Because the function returns a closure, it will memorize the outer scope, which contains the initial input parameter.


Enter fullscreen mode Exit fullscreen mode

func add(x int) func(y int) int {
return func(y int) int {
return x + y
}
}


Enter fullscreen mode Exit fullscreen mode

Now let’s try out currying and create more advanced pure functions.


Enter fullscreen mode Exit fullscreen mode

func main() {
// Create more variations
add10 := add(10)
add20 := add(20)

// Currying
fmt.Println(add10(1)) // 11
fmt.Println(add20(1)) // 21
Enter fullscreen mode Exit fullscreen mode

}


Enter fullscreen mode Exit fullscreen mode

This approach is common in functional programming, although you don’t see it often outside the paradigm.

5. Recursion

Recursion is a software pattern that is commonly employed to circumvent the use of loops. Because loops always hold an internal state to know which round they’re at, we can’t use them under the functional programming paradigm.

For example, the below snippet of code tries to calculate the factorial for a number. The factorial is the product of an integer and all the integers below it. So, the factorial of 4 is equal to 24 (= 4 * 3 * 2 * 1).

Normally, you would use a loop for this.


Enter fullscreen mode Exit fullscreen mode

func factorial(fac int) int {
result := 1
for ; fac > 0; fac-- {
result *= fac
}
return result
}


Enter fullscreen mode Exit fullscreen mode

To accomplish this within the functional programming paradigm, we need to use recursion. In other words, we’ll call the same function over and over until we reach the lowest integer for the factorial.


Enter fullscreen mode Exit fullscreen mode

func calculateFactorial(fac int) int {
if fac == 0 {
return 1
}
return fac * calculateFactorial(fac - 1)
}


Enter fullscreen mode Exit fullscreen mode

Conclusion

Let’s sum up what we learned about functional programming:

  • Although Golang supports functional programming, it wasn’t designed for this purpose, as evidenced by the lack of functions like Map, Filter, and Reduce.
  • Functional programming improves the readability of your code because functions are pure and, therefore, easy to understand
  • Pure functions are easier to test since there is no internal state that can alter the output

To learn more about the use cases of pure functions and why they matter, check out this FreeCodeCamp article about the need for pure functions for Redux reducers.

For a good overview of the differences between functional, procedural, and object-oriented programming, or if you want to understand which paradigm fits you best, I recommend reading this insightful Medium post by Lili Ouaknin Felsen.


Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Functional programming in Go appeared first on LogRocket Blog.

Top comments (2)

Collapse
 
rodiongork profile image
Rodion Gorkovenko

Well, that's very curious idea :)

Go is very far from functional language and paradigm, though it allows operating functions as variables (as C++ do).

However, with the lack of variable matching and tail-recursion, with lack of ternary operator (and really many expressions won't return value) it makes "very poor man's FP".

So honestly I'd say if one wants to practice FP, choose another language. Perhaps Erlang, which, as Go, has messages and easy funny threads out-of-the-box. And use Go for other things.

Go is good language gaining much popularity, worth of learning its concepts. FP is a kind of hype during perhaps last 20 years, as OOP was hype for bit longer. We learn by and by that real tasks don't well map to FP or OOP.

To put it simply, functional programming makes your code more readable, easier to test, and less complex due to the absence of states and mutable data.

Honestly, all this is arguable. In industry I've seen a lot of mess in projects using functional language (e.g. Scala). Main advantage of absence of state and mutability, imho, is that it makes parallel execution and multithreading very easy, as we need no syncrhonization, locks etc. But FP approach sometimes (often) pays in performance for this...

Collapse
 
jrwren profile image
Jay R. Wren

not to mention lack of either dynamic types or generics. with no generic filter/map/reduce, it makes FP in Go rather terrible. Best to not leave the rails of the idioms of Go.