Functional Programming in Go

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.

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.

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.

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

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.

<code>
name := "first name"
name := name + " last name"
</code>

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.

<code>
    firstname := "first"
    lastname := "last"
    fullname := firstname + " " + lastname
</code>

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.

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:

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


    // Add Lucas to the array
    names[2] = "Lucas"
</code>

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

<code>
    names := []string{"Tom", "Ben"}
    allNames := append(names, "Lucas")
</code>

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.

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


    // Buy five apples
    fruits["apples"] = 5
<code>

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

<code>
    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
    }
</code>

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.

<code>
func add(x int) func(y int) int {
    return func(y int) int {
        return x + y
    }
}
</code>

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

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

    // Currying
    fmt.Println(add10(1)) // 11
    fmt.Println(add20(1)) // 21
}
</code>

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.

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

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.

<code>
func calculateFactorial(fac int) int {
    if fac == 0 {
        return 1
    }
    return fac * calculateFactorial(fac - 1)
}
</code>

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

#go #programming #funtional

Functional Programming in Go
19.80 GEEK