Basics of Functional Programming in Go

Leonardo
7 min readJun 16, 2024

--

Alright, folks, let’s talk about functional programming in Go. Now, before you start yawning or worry that this will be like your college computer science lectures, let’s clear something up: we’re going to keep this as painless as possible. No fancy jargon, just straight talk and a bit of sarcasm. Because, why not?

Go isn’t the first language you think of when you hear “functional programming.” You might think of Haskell, with its pure functions and monads (don’t panic yet), or maybe JavaScript, which loves to show off with its higher-order functions and callbacks. But guess what? You can do functional programming in Go, too, and it doesn’t have to be as boring as watching paint dry.

Higher-Order Functions

First up, let’s talk about higher-order functions. These are just functions that play well with other functions, either by taking them as parameters or spitting them out as return values. In the wild world of Go, this isn’t just possible; it’s actually pretty neat.

package main

import (
"fmt"
)

func filter(numbers []int, f func(int) bool) []int {
var result []int
for _, value := range numbers {
if f(value) {
result = append(result, value)
}
}
return result
}

func isEven(n int) bool {
return n%2 == 0
}

func main() {
numbers := []int{1, 2, 3, 4}
even := filter(numbers, isEven)
fmt.Println(even) // [2, 4]
}

Did you see that? It’s like we’re giving JavaScript a run for its money.

Currying

Next on the list is currying. No, it’s not about cooking; it’s about breaking down a function that takes multiple arguments into a series of functions that each take one argument. It sounds more complicated than it is.

package main

import "fmt"

func add(a int) func(int) int {
return func(b int) int {
return a + b
}
}

func main() {
addFive := add(5)
fmt.Println(addFive(3)) // 8
}

Simple, straightforward, and it gets the job done without any frills.

Immutability

One of the hallmarks of functional programming is immutability. Once you make something, you don’t change it. Instead, you create a new thing if you need something different. This might sound a bit wasteful at first, but it actually keeps things clean and reduces side effects.

package main

import "fmt"

func main() {
obj := map[string]int{"a": 1, "b": 2}
newObj := make(map[string]int)
for k, v := range obj {
newObj[k] = v
}
newObj["b"] = 3
fmt.Println(newObj) // map[a:1 b:3]
}

Pure Functions

Pure functions are like your neat-freak friends. They don’t touch or modify anything outside their scope. What you pass in is what you work with, and what you return is the only effect they have.

package main

import "fmt"

func square(x int) int {
return x * x
}

func main() {
fmt.Println(square(5)) // 25
}

Look, no side effects. No global variables harmed in the making of this function.

Functors

In the most down-to-earth terms possible, a functor is anything you can map a function over. Think about the humble array; you apply a function to each item and get a shiny new array. In Go, we don’t have built-in generic map functions, but we can certainly make our own because we’re resourceful like that.

Let’s make a functor out of a slice of ints in Go:

package main

import "fmt"

// Functor on a slice of int
func mapInts(values []int, f func(int) int) []int {
result := make([]int, len(values))
for i, v := range values {
result[i] = f(v)
}
return result
}

func main() {
numbers := []int{1, 2, 3, 4}
squared := mapInts(numbers, func(x int) int { return x * x })
fmt.Println(squared) // [1, 4, 9, 16]
}

Look at that! Who needs built-in methods when you can flex your coding muscles like this?

Endofunctors

Now, moving on to endofunctors, which is just a fancy way of saying a functor that maps a type to the same type. In simpler terms, you start with a Go slice, and you end up with a Go slice. Same family. It’s not rocket science, just a bit of type consistency.

Our previous example of mapInts? That’s an endofunctor in disguise. It takes []int and returns []int. No type-hopping here.

Monoids

Imagine a party where everyone needs to bring a friend. Monoids are like that, but with types. They need two things: an operation that combines two of those types and a special value that acts like the most agreeable friend ever — it gets along with everyone and changes nothing about them.

In Go, we can see this in action with slices or numbers. Let’s talk numbers because they’re a bit easier on the brain:

package main

import "fmt"

// Integer addition is a monoid with zero as the identity element
func add(a, b int) int {
return a + b
}

func main() {
fmt.Println(add(5, 5)) // 10
fmt.Println(add(5, 0)) // 5
fmt.Println(add(0, 0)) // 0
}

Zero is our hero here, the identity element. It keeps the peace and leaves numbers unchanged.

Monads

Remember the meme/copypasta:

“A monad is a monoid in the category of endofunctors.”

So, when someone drops the bomb, “a monad is a monoid in the category of endofunctors,” they’re basically trying to show off their computer science vocabulary bingo skills. Here’s the breakdown: a monad is a type of programming structure that deals with types and functions in a super specific way — kind of like how some people are particular about how their coffee is made. A monoid, in the simplest terms, is about combining stuff together with a special rule that includes a do-nothing or identity element. Now, throw in endofunctors, which are like your regular old functions, but they stick to transforming things within their own little universe (category). Put it all together, and you’ve got this idea that monads can be seen as a way of sticking functions together in a sequence, but in a super self-contained way that also respects the original structure of the data. It’s like saying, “We’re going on a road trip, but only on scenic backroads, and we’ll end up right back where we started.”

Monads are the know-it-alls. They can not only handle values with context (like errors or lists) but also chain operations together in a way that passes along that context. In Go, this can be a bit awkward to mimic, but let’s talk about error handling, which is a practical use of monads.

In Haskell and functional programming, the Maybe type is a way to represent computations that might fail or values that might be absent. It is a very common and useful tool for handling optional values without resorting to error-prone practices like using null references.

The Maybe type is defined as follows in Haskell:

data Maybe a = Nothing | Just a

This definition means that Maybe is a type that can be either:

  • Nothing, representing the absence of a value.
  • Just a, where a is some value of type a, representing the presence of a value.

The Just constructor is used to wrap a value into the Maybe type. For example:

Just 5   -- Represents a Maybe Int containing the value 5
Just "Hello" -- Represents a Maybe String containing the value "Hello"

Using Just allows you to explicitly state that a value is present.

The Nothing constructor is used to represent the absence of a value. For example:

Nothing :: Maybe Int   -- Represents a Maybe Int with no value
Nothing :: Maybe String -- Represents a Maybe String with no value

Using Nothing allows you to explicitly state that no value is present.

The Maybe type is often used in functions that might fail or return optional results. In Haskell, Maybe is also a monad, which provides a way to chain operations that might fail. The bind operation (>>=) allows you to compose such operations.

Ok, enough with theory. Let’s try implementing Maybe in Go:

package main

import (
"fmt"
)

type Maybe[T any] struct {
value *T
}

func Just[T any](value T) Maybe[T] {
return Maybe[T]{value: &value}
}

func Nothing[T any]() Maybe[T] {
return Maybe[T]{value: nil}
}

func (m Maybe[T]) IsPresent() bool {
return m.value != nil
}

func (m Maybe[T]) Get() (T, bool) {
if m.value == nil {
var zero T
return zero, false
}
return *m.value, true
}

func (m Maybe[T]) Bind[U any](f func(T) Maybe[U]) Maybe[U] {
if !m.IsPresent() {
return Nothing[U]()
}
return f(*m.value)
}

func addOne(x int) Maybe[int] {
return Just(x + 1)
}

func main() {
// Create a Just Maybe
a := Just(5)

// Use Bind to chain operations
b := a.Bind(addOne).Bind(addOne).Bind(addOne)

// Check the result
if b.IsPresent() {
value, _ := b.Get()
fmt.Println("Value:", value) // Output: Value: 8
} else {
fmt.Println("No value")
}

// Create a Nothing Maybe
c := Nothing[int]()

// Use Bind with Nothing
d := c.Bind(addOne).Bind(addOne).Bind(addOne)

// Check the result
if d.IsPresent() {
value, _ := d.Get()
fmt.Println("Value:", value)
} else {
fmt.Println("No value") // Output: No value
}
}

In this example:

  • We define a Maybe type with methods to check if a value is present and to get the value.
  • The Just function creates a Maybe with a value.
  • The Nothing function creates a Maybe without a value.
  • The Bind method allows chaining operations that return Maybe types.
  • We demonstrate usage with a simple addOne function and show how Bind can be used to chain operations.

This implementation provides a monad-like structure for handling optional values in Go, allowing for functional-style chaining of operations.

Conclusion

Functional programming in Go might not be the poster child for the functional paradigm, but it’s entirely doable and can even be fun. Who knew, right? By now, you should see that Go can be just as functional as the next language, and with a bit of effort, you can write some clean, efficient, and robust code.

Go ahead, give it a try. What’s the worst that could happen? You might even like it. And if not, at least you’ve learned something new. Happy coding, and may your functions always be pure!

--

--

Leonardo

Software developer, former civil engineer. Musician. Free thinker. Writer.