Lesson 14. First-class functions

After reading lesson 14, you’ll be able to

  • Assign functions to variables
  • Pass functions to functions
  • Write functions that create functions

In Go you can assign functions to variables, pass functions to functions, and even write functions that return functions. Functions are first-class—they work in all the places that integers, strings, and other types work.

This lesson explores some potential uses of first-class functions as part of a theoretical Rover Environmental Monitoring Station (REMS) program that reads from (fake) temperature sensors.

Consider this

A recipe for tacos calls for salsa. You can either turn to page 93 of the cookbook to make homemade salsa or open a jar of salsa from the store.

First-class functions are like tacos that call for salsa. As code, the makeTacos function needs to call a function for the salsa, whether that be makeSalsa or openSalsa. The salsa functions could be used independently as well, but the tacos won’t be complete without salsa.

Other than recipes and temperature sensors, what’s another example of a function that can be customized with a function?

14.1. Assigning functions to variables

The weather station sensors provide an air temperature reading from 150–300° K. You have functions to convert Kelvin to other temperature units once you have the data, but unless you have a sensor attached to your computer (or Raspberry Pi), retrieving the data is a bit problematic.

For now you can use a fake sensor that returns a pseudorandom number, but then you need a way to use realSensor or fakeSensor interchangeably. The following listing does just that. By designing the program this way, different real sensors could also be plugged in, for example, to monitor both ground and air temperature.

Listing 14.1. Interchangeable sensor functions: sensor.go
package main

import (
    "fmt"
    "math/rand"
)

type kelvin float64

func fakeSensor() kelvin {
    return kelvin(rand.Intn(151) + 150)
}

func realSensor() kelvin {
    return 0                        1
}

func main() {
    sensor := fakeSensor            2
    fmt.Println(sensor())

    sensor = realSensor
    fmt.Println(sensor())
}

  • 1 To-do: implement a real sensor
  • 2 Assigns a function to a variable

In the previous listing, the sensor variable is assigned to the fakeSensor function itself, not the result of calling the function. Function and method calls always have parentheses, such as fakeSensor(), which isn’t the case here.

Now calling sensor() will effectively call either realSensor or fakeSensor, depending on which function sensor is assigned to.

The sensor variable is of type function, where the function accepts no parameters and returns a kelvin result. When not relying on type inference, the sensor variable would be declared like this:

var sensor func() kelvin
Note

You can reassign sensor to realSensor in listing 14.1 because it matches the function signature of fakeSensor. Both functions have the same number and type of parameters and return values.

Quick check 14.1

1

How can you distinguish between assigning a function to a variable versus assigning the result of calling the function?

2

If there existed a groundSensor function that returned a celsius temperature, could it be assigned to the sensor in listing 14.1?

QC 14.1 answer

1

Function and method calls always have parentheses (for example, fn()) whereas the function itself can be assigned by specifying a function name without parentheses.

2

No. The parameters and return values must be of the same type to reassign the sensor variable. The Go compiler will report an error: cannot use groundSensor in assignment.

 

14.2. Passing functions to other functions

Variables can refer to functions, and variables can be passed to functions, which means Go allows you to pass functions to other functions.

To log temperature data every second, listing 14.2 declares a new measureTemperature function that accepts a sensor function as a parameter. It calls the sensor function periodically, whether it’s a fakeSensor or a realSensor.

The ability to pass functions around gives you a powerful way to split up your code. If not for first-class functions, you would likely end up with measureRealTemperature and measureFakeTemperature functions containing nearly identical code.

Listing 14.2. A function as a parameter: function-parameter.go
package main

import (
    "fmt"
    "math/rand"
    "time"
)

type kelvin float64

func measureTemperature(samples int, sensor func() kelvin) {     1
    for i := 0; i < samples; i++ {
        k := sensor()
        fmt.Printf("%v° K
", k)
        time.Sleep(time.Second)
    }
}

func fakeSensor() kelvin {
    return kelvin(rand.Intn(151) + 150)
}

func main() {
    measureTemperature(3, fakeSensor)                           2
}

  • 1 measureTemperature accepts a function as the second parameter.
  • 2 Passes the name of a function to a function

The measureTemperature function accepts two parameters, with the second parameter being of type func() kelvin. This declaration looks like a variable declaration of the same type:

var sensor func() kelvin

The main function is able to pass the name of a function to measureTemperature.

Quick check 14.2

Q1:

How is the ability to pass functions to other functions beneficial?

QC 14.2 answer

1:

First-class functions provide another way to divide and reuse code.

 

14.3. Declaring function types

It’s possible to declare a new type for a function to condense and clarify the code that refers to it. You used the kelvin type to convey a unit of temperature rather than the underlying representation. The same can be done for functions that are being passed around:

type sensor func() kelvin

Rather than a function that accepts no parameters and returns a kelvin value, the code is about sensor functions. This type can be used to condense other code, so the declaration

func measureTemperature(samples int, s func() kelvin)

can now be written like this:

func measureTemperature(samples int, s sensor)

In this example, it may not seem like an improvement, as you now need to know what sensor is when looking at this line of code. But if sensor were used in several places, or if the function type had multiple parameters, using a type would significantly reduce the clutter.

Quick check 14.3

Q1:

Rewrite the following function signature to use a function type:

func drawTable(rows int, getRow func(row int) (string, string))

QC 14.3 answer

1:

type getRowFn func(row int) (string, string)

func drawTable(rows int, getRow getRowFn)

 

14.4. Closures and anonymous functions

An anonymous function, also called a function literal in Go, is a function without a name. Unlike regular functions, function literals are closures because they keep references to variables in the surrounding scope.

You can assign an anonymous function to a variable and then use that variable like any other function, as shown in the following listing.

Listing 14.3. Anonymous function: masquerade.go
package main

import "fmt"

var f = func() {                                 1
    fmt.Println("Dress up for the masquerade.")
}

func main() {
    f()                                          2
}

  • 1 Assigns an anonymous function to a variable
  • 2 Prints Dress up for the masquerade.

The variable you declare can be in the scope of the package or within a function, as shown in the next listing.

Listing 14.4. Anonymous function: funcvar.go
package main

import "fmt"

func main() {
    f := func(message string) {         1
        fmt.Println(message)
    }
    f("Go to the party.")               2
}

  • 1 Assigns an anonymous function to a variable
  • 2 Prints Go to the party.

You can even declare and invoke an anonymous function in one step, as shown in the following listing.

Listing 14.5. Anonymous function: anonymous.go
package main

import "fmt"

func main() {
    func() {                                1
        fmt.Println("Functions anonymous")
    }()                                     2
}

  • 1 Declares an anonymous function
  • 2 Invokes the function

Anonymous functions can come in handy whenever you need to create a function on the fly. One such circumstance is when returning a function from another function. Although it’s possible for a function to return an existing named function, declaring and returning a new anonymous function is much more useful.

In listing 14.6 the calibrate function adjusts for errors in air temperature readings. Using first-class functions, calibrate accepts a fake or real sensor as a parameter and returns a replacement function. Whenever the new sensor function is called, the original function is invoked, and the reading is adjusted by an offset.

Listing 14.6. Sensor calibration: calibrate.go
package main

import "fmt"

type kelvin float64

// sensor function type
type sensor func() kelvin

func realSensor() kelvin {
    return 0                             1
}

func calibrate(s sensor, offset kelvin) sensor {
    return func() kelvin {               2
         return s() + offset
    }
}

func main() {
    sensor := calibrate(realSensor, 5)
    fmt.Println(sensor())                3
}

  • 1 To-do: implement a real sensor
  • 2 Declares and returns an anonymous function
  • 3 Prints 5

The anonymous function in the preceding listing makes use of closures. It references the s and offset variables that the calibrate function accepts as parameters. Even after the calibrate function returns, the variables captured by the closure survive, so calls to sensor still have access to those variables. The anonymous function encloses the variables in scope, which explains the term closure.

Because a closure keeps a reference to surrounding variables rather than a copy of their values, changes to those variables are reflected in calls to the anonymous function:

var k kelvin = 294.0

sensor := func() kelvin {
    return k
}
fmt.Println(sensor())        1

k++
fmt.Println(sensor())        2

  • 1 Prints 294
  • 2 Prints 295

Keep this in mind, particularly when using closures inside for loops.

Quick check 14.4

1

What’s another name for an anonymous function in Go?

2

What do closures provide that regular functions don’t?

QC 14.4 answer

1

An anonymous function is also called a function literal in Go.

2

Closures keep references to variables in the surrounding scope.

 

Summary

  • When functions are treated as first-class, they open up new possibilities for splitting up and reusing code.
  • To create functions on the fly, use anonymous functions with closures.

Let’s see if you got this...

Experiment: calibrate.go

Type listing 14.6 into the Go Playground to see it in action:

  • Rather than passing 5 as an argument to calibrate, declare and pass a variable. Modify the variable and you’ll notice that calls to sensor() still result in 5. That’s because the offset parameter is a copy of the argument (pass by value).
  • Use calibrate with the fakeSensor function from listing 14.2 to create a new sensor function. Call the new sensor function multiple times and notice that the original fakeSensor is still being called each time, resulting in random values.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset