Map and filter
The next code example demonstrates the use of a few standard intermediate functions: map and filter.
The code in this example can be copy/pasted into The Go playground, which is a service that takes your Go program, compiles, links, and runs your program with the latest version of Go inside a sandbox and then returns the output to the screen. You can find it at https://play.golang.org/.
Executable commands must always use package main. We can separate each import statement on a separate line for readability.
External packages can be referenced using their remote GitHub repository path. We can preface long package names with a shorter alias. The go_utils package can now be referenced with the u letter. Note that if we aliased a package name with _, its exported functions can be referenced directly in our Go code without indicating which package it came from:
package main
import (
"fmt"
"log"
"strings"
"errors"
u "github.com/go-goodies/go_utils"
)
iota: A Go identifier used in const declarations that represents successive untyped integer constants. It is reset to 0 whenever the reserved word const appears:
const (
SMALL = iota // 0
MEDIUM // 1
LARGE // 2
)
We can apply expressions to iota to set increment values greater than 1. We do this as discussed in the next section.
Let's define a type of ints called WordSize and use an iota expression to create an enumeration from our constants. The first iota elements are assigned values that start at 0 and then increase by 1. Since we multiplied the iota element by 6, the sequence will look like 0, 6, 12, 18, and so on. We explicitly assign the value of 50 to the last element in the enumeration:
type WordSize int
const (
ZERO WordSize = 6 * iota
SMALL
MEDIUM
LARGE
XLARGE
XXLARGE WordSize = 50
SEPARATOR = ", "
)
The ChainLink type allows us to chain function/method calls. It also keeps data internal to ChainLink, avoiding the side effect of mutated data:
type ChainLink struct {
Data []string
}
The Value() method will return the value of the referenced element or link in the chain:
func (v *ChainLink) Value() []string {
return v.Data
}
Let's define stringFunc as a function type. This first-class method is used in the following code as a parameter to the Map function:
type stringFunc func(s string) (result string)
The Map function uses stringFunc to transform (up-case) each string in the slice:
func (v *ChainLink)Map(fn stringFunc) *ChainLink {
var mapped []string
orig := *v
for _, s := range orig.Data {
mapped = append(mapped, fn(s))
}
v.Data = mapped
return v
}
This line is worth repeating:
mapped = append(mapped, fn(s))
We execute the fn() function parameter against each element in the slice.
The Filter function uses embedded logic to filter the slice of strings. We could have chosen to use a first-class function, but this implementation is faster:
func (v *ChainLink)Filter(max WordSize) *ChainLink {
filtered := []string{}
orig := *v
for _, s := range orig.Data {
if len(s) <= int(max) { // embedded logic
filtered = append(filtered, s)
}
}
v.Data = filtered
return v
}
What's wrong, from a pure FP perspective, about our filter function in the preceding code?
- We are using an imperative loop
- We are saving the filtered results to the Data field our ChainLink structure
Why not use recursion? We discussed this earlier. The short version is that until Go gets TCO we need to avoid recursion if our list of elements we're processing could be over a few thousand elements.
Why are we storing the filtered data rather than returning it? Good question. This implementation of the filter function serves as a learning lesson. It shows us how we can chain functions in a non-pure FP way. We'll look at an improved filter implementation in the next chapter. Here's sneak peek:
func (cars Collection) Filter(fn FilterFunc) Collection {
filteredCars := make(Collection, 0)
for _, car := range cars {
if fn(car) {
filteredCars = append(filteredCars, car)
}
}
return filteredCars
}
Let's display our constants using a here-doc with interpolation. Note that the first argument to the fmt.Printf statement is our here-doc, constants, and the remaining arguments are interpolated in constants.
func main() {
constants := `
** Constants ***
ZERO: %v
SMALL: %d
MEDIUM: %d
LARGE: %d
XLARGE: %d
XXLARGE: %d
`
fmt.Printf(constants, ZERO, SMALL, MEDIUM, LARGE, XLARGE, XXLARGE)
The output will be as follows:
** Constants ***
ZERO: 0
SMALL: 6
MEDIUM: 12
LARGE: 18
XLARGE: 24
XXLARGE: 50
Let's initialize ChainLink with our slice of words:
words := []string{
"tiny",
"marathon",
"philanthropinist",
"supercalifragilisticexpialidocious"}
data := ChainLink{words};
fmt.Printf("unfiltered: %#v\n", data.Value())
The output will be as follows:
unfiltered: []string{"tiny", "marathon", "philanthropinist", "supercalifragilisticexpialidocious"}
Now, let's filter our list of words:
filtered := data.Filter(SMALL)
fmt.Printf("filtered: %#vn", filtered)
The output will be as follows:
filtered: &main.ChainLink{Data:[]string{"tiny"}}
Next, let's apply the ToUpper mapping to our small-sized words:
fmt.Printf("filtered and mapped (<= SMALL sized words): %#vn",
filtered.Map(strings.ToUpper).Value())
The output will be as follows:
filtered and mapped (<= SMALL sized words): []string{"TINY"}
Let's apply a MEDIUM filter and the ToUpper filter:
data = ChainLink{words}
fmt.Printf("filtered and mapped (<= MEDIUM and smaller sized words): %#vn",
data.Filter(MEDIUM).Map(strings.ToUpper).Value())
The output will be as follows:
filtered and mapped (<= MEDIUM and smaller sized words): []string{"TINY", "MARATHON"}
Next, let's apply our XLARGE filter and map then ToUpper:
data = ChainLink{words}
fmt.Printf("filtered twice and mapped (<= LARGE and smaller sized words):
%#vn",
data.Filter(XLARGE).Map(strings.ToUpper).Filter(LARGE).Value())
The output will be as follows:
filtered twice and mapped (<= LARGE and smaller sized words): []string{"TINY", "MARATHON", "PHILANTHROPINIST"}
Now, let's apply our XXLARGE filter and map with ToUpper:
data = ChainLink{words}
val := data.Map(strings.ToUpper).Filter(XXLARGE).Value()
fmt.Printf("mapped and filtered (<= XXLARGE and smaller sized words): %#vn",
val)
The output will be as follows:
mapped and filtered (<= XXLARGE and smaller sized words): []string{"TINY", "MARATHON", "PHILANTHROPINIST", "SUPERCALIFRAGILISTICEXPIALIDOCIOUS"}
The output will be as follows:
** Constants ***
ZERO: 0
SMALL: 6
MEDIUM: 12
LARGE: 18
XLARGE: 24
XXLARGE: 50
Here, we use the Join() function to join the items in the list to help with formatting our output:
fmt.Printf("norig_data : %vn", u.Join(orig_data, SEPARATOR))
fmt.Printf("data: %vnn", u.Join(data.Value(), SEPARATOR))
The output will be as follows:
orig_data : tiny, marathon, philanthropinist, supercalifragilisticexpialidocious
data: TINY, MARATHON, PHILANTHROPINIST, SUPERCALIFRAGILISTICEXPIALIDOCIOUS
Now, let's compare our original collection of words with the value that we passed through our chain of functions to see whether there were side effects:
This is what your terminal console should look like: