Practice with Functional Programming in Go

Written by kliukovkin | Published 2022/03/15
Tech Story Tags: go | functional-programming | golang | programming | coding | learn-to-code | learning-to-code | hackernoon-top-story

TLDRGo is a similar to JavaScript in case of functional programming. Though you can use different aspects of functional programming like functions as first class citizens, closure, recursion, memorization and so on - keep in mind that go is struct language. Don't try to write all your go code in functional paradigm.via the TL;DR App

While working with a new language you will always try to find some similarities with languages that you already know. I’m experienced in JavaScript and Java and can conclude that Go is more similar to JS. Why is that so? Well, in my opinion, this is because of the supporting some functional paradigm features.

Pure functions

It is a basic entity of functional programming. From wiki:

the function return values are identical for identical arguments

Here is an example of a pure function:

func multiply(a, b int) int {
  return a * b
}

Functions as First-Class Citizens

According to the wiki, it means that:

a given entity (such as a function) supports all the operational properties inherent to other entities; properties such as being able to be assigned to a variable, passed around as a function argument, returned from a function, etc.

We can return a function:

package main

import "fmt"

func main() {
	myFunc := makeCounter()
	myFunc()
	myFunc()
}

func makeCounter() func() {
	counter := 0
	return func() {
		fmt.Println(counter)
		counter++
	}
}

What should you notice here is that makeCounter function not only returns a new anonymous function but also got a variable that saves in anonymous function lexical closure, just like in JavaScript.

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment.

Wrapping

At the time this article is written, Go doesn’t support generics through Go version 1.18 which will include support of generic types should be released in March 2022

Go 1.18 is not yet released. These are work-in-progress release notes. Go 1.18 is expected to be released in March 2022.

Despite this fact you can also use some wrapper functions, for example, if you want to provide some data to the output while invoking certain functions like so:

func multiply(a, b int) int {
	return a * b
}

func wrapWithLogs(fn func(a, b int) int) func(a,b int) int {
	return func(a, b int) int {
		start := time.Now()
		r := fn(a, b)
		duration := time.Since(start)
		fmt.Println("function took ", duration)
		return r
	}
}

Of course, it will be much more effective using generics. So we need just to wait for a while until the new release will be published :)

If you want to get the name of the function dynamically you can also use:

runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()

Wrapping opens a door to use some cool things like memoization

Memoization

In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

Like it said, if you have some heavy calculation - it is always better to cache already computed results somewhere and use them instead of doing the same calculation again.

func sum(a,b int) int {
	return a + b
}

func memo(fn func(a, b int) int) func(a, b int) int {
	cache := make(map[string]int)
	return func(a, b int) int {
		key := strconv.Itoa(a) + " " + strconv.Itoa(b)
		v, ok := cache[key]
		if !ok {
			fmt.Println("calculating...")
			cache[key] = fn(a, b)
			v = cache[key]
		}
		return v
	}
}

func main() {
	mSum := memo(sum)
	fmt.Println(mSum(2, 3))
	fmt.Println(mSum(2, 3))
}

The result will be:

calculating... 5 5

Of course, you can avoid wrapper function and write memorization logic directly inside your function. But still you will use closure to store cache inside.

Recursion

We can use recursion if we need to calculate something like factorial:

func funcFactorial(num int) int {
	if num == 0 {
		return 1
	}
	return num * funcFactorial(num-1)
}

But please be aware of using recursion whenever possible. The problem is that in most cases it is better to use some stack or queue instead of recursion. Usually, limits of memory lie further than the limits of the call stack.

Currying

Remember we discussed closure in the first example? Currying is about taking a parameter and returning it in a closure. For example, we got a function multiply:

func multiply(a, b int) int {
  return a * b
}

We can call this function like multiply(2, 3). With currying, we can call this function in a way multiply(2)(3) . To do that we need to rewrite our main function:

func multiply(a int) func(b int) int {
  return func(b int) int {
    return a * b
  }
}

But rewriting existing function just to use currying is not a good practice. Using some wrapper function we can do it in a better way:

func multiply(a, b int) int {
	return a * b
}

func curry(a int) func(b int) int {
	return func(b int) int {
		return multiply(a, b)
	}
}

Conclusion

Go supports some features of the functional paradigm to make things easier. But don’t forget that it is structural language and there is no need to try to write Go code in Functional or OOP style. You will lose in both cases.


Written by kliukovkin | Developer.
Published by HackerNoon on 2022/03/15