Higher-Order Function in Go

I worked as a front-end engineer in the early days of the current company. We had used Angular for the time being and moved to React. React was a framework that actively used higher-order functions. I always have thought Javascript is the most "sophisticated" language and the glitzy feast of higher-order functions made me puzzled. Since then, I have had more time in the backend code, and Go become my first language. Go is a solid and rigid language, and I could experience only so many non-idiomatic patterns; you might have seen the endless series of err := ... and if err != nils.

But last week, I reviewed a colleague's code and found a beautiful higher-order function usage. As a little more improved software engineer, I wanted to put together what I've learned about the higher-order function.

Although Go is more an imperative language, it has some functional programming features. Please check Dylan Meeus's GopherCon 2020 presentation if you are interested. Many of the sample codes in this article also got much insight from his presentation.

A higher-order function is a function that 1) takes functions as arguments or 2) returns a function as a result. Taking function arguments are easier to understand. The sort.Slice function is an example.

func Slice(x interface{}, less func(i, j int) bool)
  • x is a slice to be sorted.
  • less is a function that returns true when i is less than j and false otherwise.

Here is an example.

package main

import (
	"fmt"
	"sort"
)

func main() {
	var (
		input = []int{4, 3, 2, 12, 18, 9}
		less = func(i, j int) bool {
			return input[i] < input[j]
		}
	)

	sort.Slice(input, less)
	fmt.Printf("sorted: %v", input)
}

You can see the sorted result.

sorted: [2 3 4 9 12 18]

Then why do we want to return a function? There can be many causes, but IMHO, the most exciting usage is curring. By definition, currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each takes a single argument. But I think currying is a method to reduce the number of function arguments to embed dependencies by returning a closure.

A Closure is a technique for implementing lexically scoped name binding in a language with first-class functions. A bit pedantic, isn't it? We can think the closure is a function that embeds an external environment. Still hard to grasp? Practically, in many programming languages, it means an inner function that can reference outer scopes' variables.

Some languages such as Javascript or Swift say an anonymous function or an arrow function as a closure, but it is because those functions have this characteristic.
func dropHead(s string) string {
	drop := func(i int) string {
		return s[i:]
	}
	return drop(1)
}

In this example, drop is a closure having access to the outer function dropHead's variable (the argument s).
How can we use closures for currying? Let's assume that we want a function that takes two arguments: 1) the greeting and 2) a person's name, then return a full greeting.

func greet(greeting, person string) string {
	return fmt.Sprintf("%s %s\n, greeting, person)
}

// greet("Hello", "Diko") -> "Hello Diko"
// greet("Hola", "Jin") -> "Hola Jin"

But if we only use "Hello" for the greet always, giving "Hello" to all function calls would be a bit redundant. We can make a new function embedding "Hello" as its greeting by currying.

func prefixGreet(greeting string) func(string) string {
	return func(n string) string {
		return greet(greeting, n)
	}
}

englishGreet := prefixGreet("Hello")
spanishGreet := prefixGreet("Hola")

// englishGreet("Diko") -> "Hello Diko"
// spanishGreet("Jin") -> "Hola Jin"

One example of curring in Go is the ServerOption of gRPC. Many "option" implementations have similar patterns in many languages.

type serverOptions struct {
	creds                 credentials.TransportCredentials
	codec                 baseCodec
...
	headerTableSize       *uint32
	numServerWorkers      uint32
}

// A ServerOption sets options such as credentials, codec and keepalive parameters, etc.
type ServerOption interface {
	apply(*serverOptions)
}

// funcServerOption wraps a function that modifies serverOptions into an
// implementation of the ServerOption interface.
type funcServerOption struct {
	f func(*serverOptions)
}

func (fdo *funcServerOption) apply(do *serverOptions) {
	fdo.f(do)
}

func newFuncServerOption(f func(*serverOptions)) *funcServerOption {
	return &funcServerOption{
		f: f,
	}
}

The serverOptions is the actual struct that defines the server options in the implementation, but it is private. We have a public interface ServerOption. Interestingly, the interface only has one "private" method apply. It means that the interface consumer doesn't need to know its method; only the name (or type) ServerOption matters.


The funcServerOption is a struct that implements the ServerOption interface. So, it has the apply method, and it just calls the struct's private member function f with an argument of *serverOptions. The code is already somewhat complicated, and it's like migraine is coming up over the horizon, but there is the last piece: a private function newFuncServerOption. The function gets a function and sets it in a funcServerOption - which conforms to ServerOption - and returns it. It is the core tool to implement the actual server option; we can implement an option as follow.

// MaxSendMsgSize returns a ServerOption to set the max message size in bytes the server can send.
func MaxSendMsgSize(m int) ServerOption {
	return newFuncServerOption(func(o *serverOptions) {
		o.maxSendMessageSize = m
	})
}

The MaxSendMsgSize function returns a ServerOption interface. Then interface implementation (funcServerOption) has a closure embedding the maximum message size(m). With all settings and efforts, giving options in the gRPC server initialization is expressed clearly.

import "google.golang.org/grpc"

...
		opts = []grpc.ServerOption{
			grpc.MaxSendMsgSize(1024),
		}

		server := grpc.NewServer(opts...)
...

Then grpc.NewServer implementation calls the apply method of each ServerOptions with its default server options.

func NewServer(opt ...ServerOption) *Server {
	opts := defaultServerOptions
	for _, o := range opt {
		o.apply(&opts)
	}
...

Assuming that you have many dependencies (or arguments) for a function, many of them could be fixed. Then embed them in a closure and reuse them everywhere using the higher-order function returning the closure. I think this might be the most elegant and sophisticated usage of the higher-order function and beneficial once you're accustomed to it.