background-shape
feature-image

Functional programming is a programming paradigm that emphasizes the use of functions to transform data and minimize the use of shared state and mutable data. In Go (also known as Golang), functional programming can be achieved using first-class functions, anonymous functions, and higher-order functions.

Golang is a multi-paradigm programming language. As a Golang programmer why uses functional programming?

Golang is not a functional language but has a lot of features that enable us to applies functional principles in the development, turning our code more elegant, concise, maintainable, easier to understand and test.

Here are some examples of functional programming in Go:

First-class functions

In Go, functions are first-class citizens, which means that they can be assigned to variables, passed as arguments to other functions, and returned as values from functions.

Here’s an example of a function that takes a function as an argument and returns a function as a result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

// The function `add` takes an integer and returns a function that takes another integer and returns an integer.
func add(x int) func(int) int {
    return func(y int) int {
        return x + y
    }
}

func main() {
    // Assign the result of `add` to a variable.
    plusTwo := add(2)

    // Call the function stored in `plusTwo`.
    fmt.Println(plusTwo(3)) // Output: 5
}

Anonymous functions

Go also supports anonymous functions, also known as lambda functions or closures. Anonymous functions are functions that are defined and called in the same place, without a name.

Here’s an example of an anonymous function that is passed as an argument to the fmt.Println function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    // Define an anonymous function and pass it as an argument to `fmt.Println`.
    fmt.Println(func(x int) int {
        return x * x
    }(2)) // Output: 4
}

Higher-order functions

Higher-order functions are functions that take one or more functions as arguments or return a function as a result. In Go, we can use higher-order functions to abstract common patterns of function usage, such as iterating over a collection of data and applying a function to each element.

Here’s an example of a higher-order function map that applies a given function to each element of a slice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
    // Define a slice of integers.
    numbers := []int{1, 2, 3, 4, 5}

    // Use the `map` function to apply the `square` function to each element of the slice.
    squares := map(square, numbers)

    fmt.Println(squares) // Output: [1 4 9 16 25]
}

// The `square` function returns the square of an integer.
func square(x int) int {
    return x * x
}

Functional design patterns are reusable solutions to common problems in functional programming. In Go, several design patterns from the functional programming world can be implemented using the language’s support for first-class functions and anonymous functions.

Now, we’ll look at three of these design patterns: monads, functors, and monoids.

We’ll see how these patterns can help us write cleaner, more expressive code, and we’ll look at examples of how to use them in Go.

Monads

A monad is a design pattern that allows us to chain operations together in a pipeline, while abstracting away the details of how the operations are performed. In functional programming, a monad is often used to represent computations that have side effects, such as reading from a file or making an HTTP request.

In Go, we can use anonymous functions and closures to implement monads. Here’s an example of a monad that reads a file and returns the contents as a string:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
    "bufio"
    "fmt"
    "os"
)

// The `ReadFile` function returns a monad that reads a file and returns its contents as a string.
func ReadFile(path string) func() (string, error) {
    return func() (string, error) {
        // Open the file.
        file, err := os.Open(path)
        if err != nil {
            return "", err
        }
        defer file.Close()

        // Read the contents.
        var contents string
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            contents += scanner.Text() + "\n"
        }
        if err := scanner.Err(); err != nil {
            return "", err
        }

        return contents, nil
    }
}

func main() {
    // Define the path to the file.
    path := "hello.txt"

    // Use the monad to read the file.
    contents, err := ReadFile(path)()
    if err != nil {
        panic(err)
    }

    fmt.Println(contents)
}

In this example, the ReadFile function returns a function that performs the file reading operation when called. This allows us to chain multiple monads together and delay the execution of the operations until the final result is needed.

Functors

A functor is a design pattern that represents a container or structure that can be mapped over. In functional programming, a functor is often used to apply a function to each element of a collection and return a new collection with the transformed elements.

In Go, we can use higher-order functions and anonymous functions to implement functors. Here’s an example of a functor that maps a function over a slice of integers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

// The `IntSlice` type represents a functor that maps over a slice of integers.
type IntSlice struct {
    slice []int
}

// The `Map` method applies a given function to each element of the slice and returns a new slice with the transformed elements.
func (s IntSlice) Map(f func(int) int) IntSlice {
    var result []int
    for _, x := range s.slice {
        result = append(result, f(x))
    }
    return IntSlice{result}
}

func main() {
    // Define a functor.
    numbers := IntSlice{[]int{1, 2, 3, 4, 5}}   
    // Use the `Map` method to apply the `square` function to each element of the slice.
    squares := numbers.Map(func(x int) int {
        return x * x
    })

    fmt.Println(squares.slice) // Output: [1 4 9 16 25]
}

In this example, the IntSlice type represents a functor that maps over a slice of integers. The Map method takes a function as an argument and applies it to each element of the slice, returning a new IntSlice with the transformed elements.

Monoids

A monoid is a design pattern that represents an associative binary operation with an identity element. In functional programming, a monoid is often used to combine values in a way that is both commutative and associative, such as adding numbers or concatenating strings.

In Go, we can use anonymous functions and the interface type to implement monoids. Here’s an example of a monoid that represents integer addition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

// The Monoid interface represents a monoid.
type Monoid interface {
Identity() interface{}
    Op(interface{}) interface{}
}

// The IntAddition type represents integer addition as a monoid.
type IntAddition struct{}

// The Identity method returns the identity element for integer addition (0).
func (IntAddition) Identity() interface{} {
    return 0
}

// The Op method returns the result of performing integer addition on two integers.
func (IntAddition) Op(x interface{}) interface{} {
    return func(y interface{}) interface{} {
        return x.(int) + y.(int)
    }
}

func main() {
    // Define a monoid.
    addition := IntAddition{}

    // Use the monoid to add some numbers.
    sum := addition.Op(1)(2)(3)(4)

    fmt.Println(sum) // Output: 10
}

In this example, the IntAddition type represents integer addition as a monoid. The Identity method returns the identity element (0), and the Op method returns a function that performs integer addition on two integers. By calling the Op method multiple times, we can chain the addition operations together and delay the evaluation until the final result is needed.

I hope these examples give you a good overview of monads, functors, and monoids in Go.

These design patterns can help you write more expressive and reusable code, and they are a powerful tool in the functional programming toolkit.

Case study: Implementing a functional pipeline

One common use case for functional programming in Go is implementing a pipeline, which is a series of functions that transform and process data.

Here’s an example of a pipeline that reads a CSV file, filters the rows based on a condition, and calculates the sum of a column:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import (
    "bufio"
    "encoding/csv"
    "fmt"
    "os"
    "strconv"
)

// Define the pipeline functions.

// The `readCSV` function reads a CSV file and returns a slice of rows.
func readCSV(path string) [][]string {
    // Open the file.
    file, err := os.Open(path)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // Create a CSV reader.
    reader := csv.NewReader(bufio.NewReader(file))

    // Read all the rows.
    rows, err := reader.ReadAll()
    if err != nil {
        panic(err)
    }

    return rows
}

// The `filter` function takes a slice of rows and a predicate function, 
// and returns a new slice with the rows that satisfy the predicate.
func filter(rows [][]string, predicate func(row []string) bool) [][]string {
    var result [][]string
    for _, row := range rows {
        if predicate(row) {
            result = append(result, row)
        }
    }
    return result
}

// The `sum` function takes a slice of rows and a column index, 
// and returns the sum of the values in that column.
func sum(rows [][]string, col int) int {
    var total int
    for _, row := range rows {
        value, err := strconv.Atoi(row[col])
        if err != nil {
            panic(err)
        }
        total += value
    }
    return total
}

func main() {
    // Define the path to the CSV file.
    path := "sales.csv"

    // Define the predicate function for the filter step.
    predicate := func(row []string) bool {
        return row[3] == "NY"
    }

    // Define the column index for the sum step.
    col := 5

    // Run the pipeline.
    rows := readCSV(path)
    filteredRows := filter(rows, predicate)
    total := sum(filteredRows, col)

    fmt.Println(total)
}

In this example, the pipeline reads a CSV file, filters the rows based on a condition, and calculates the sum of a column.

The pipeline consists of three functions: readCSV, filter, and sum. Each function takes some input and returns some output, and the output of one function is used as the input for the next function.

By composing these functions in this way, we can create a complex transformation of data with a clear and concise codebase.

Conclusion

As you can tell, Golang helps you write in functional style but it doesn’t force you to it.

Writing in a functional style enhances your code and makes it more self-documented. Actually it will make it more thread-safe also.

The main support for FP in Golang comes from the use of closures, iterators, and generators.

Golang still lacks an important aspect of FP: Map, Filter, Reduce, Immutable and Generic types, Pattern Matching and Tails Recursion.

There should be more work on tail recursion optimization, to encourage developers to use recursion.

References : https://medium.com/@geisonfgfg/functional-go-bc116f4c96a4