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 != nil
s.
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 ServerOption
s 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.