Description
This example demonstrates the use of a few standard functional idioms, map and filter.
It demonstrates how to chain/compose them in a syntactically pleasing manner and provides some discussion on Function Programming (FP) and how FP can be accomplished using Go.
Enums are also covered, as well as the use of the Join function from the go-goodies/go_utils package.
Golang Features
This golang code sample demonstrates the following go language features:
- enum with iota
- heredoc with interpolation
- methods
- range iteration
- string slice literal initialization
- first-class function
3rd Party Libraries
This golang code sample uses the following third party libraries:
Code Example
package main
import (
"fmt"
"log"
"strings"
"errors"
u "github.com/go-goodies/go_utils"
)
// enums indicating number of characters for that type of word
// ex: a TINY word has 4 or fewer characters
const (
TEENINY WordSize = 1
SMALL WordSize = 4 << iota
MEDIUM // assigned 8 from iota
LARGE // assigned 16 from iota
XLARGE WordSize = 32000
)
type WordSize int
func (ws WordSize) String() string {
var s string
if ws&TEENINY == TEENINY {
s = "TEENINY"
}
return s
}
// ChainLink 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
}
func (v *ChainLink)Value() []string {
return v.Data
}
// stringFunc is a first-class method, used as a parameter to _map
type stringFunc func(s string) (result string)
// _map uses stringFunc to modify (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)) // first-class function
}
v.Data = mapped
return v
}
// _filter uses embedded logic to filter the slice of strings
// Note: We could have chosen to use a first-class function
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
}
func main() {
nums := []string{
"tiny",
"marathon",
"philanthropinist",
"supercalifragilisticexpialidocious"}
data := ChainLink{nums};
orig_data := data.Value()
fmt.Printf("unfiltered: %#v\n", data.Value())
filtered := data._filter(MEDIUM)
fmt.Printf("filtered: %#v\n", filtered)
fmt.Printf("filtered and mapped (MEDIUM sized words): %#v\n",
filtered._map(strings.ToUpper).Value())
data = ChainLink{nums}
fmt.Printf("filtered and mapped (MEDIUM sized words): %#v\n",
data._filter(MEDIUM)._map(strings.ToUpper).Value())
data = ChainLink{nums}
fmt.Printf("filtered twice and mapped (SMALL sized words): %#v\n",
data._filter(MEDIUM)._map(strings.ToUpper)._filter(SMALL).Value())
data = ChainLink{nums}
val := data._map(strings.ToUpper)._filter(XLARGE).Value()
fmt.Printf("mapped and filtered (XLARGE sized words): %#v\n", val)
// heredoc with interpoloation
constants := `
** Constants ***
SMALL: %d
MEDIUM: %d
LARGE: %d
XLARGE: %d
`
fmt.Printf(constants, SMALL, MEDIUM, LARGE, XLARGE)
fmt.Printf("TEENINY: %s\n\n", TEENINY)
fmt.Printf("Join(nums, \"|\") : %v\n", u.Join(nums, "|"))
fmt.Printf("Join(orig_data, \"|\"): %v\n", u.Join(orig_data, "|"))
fmt.Printf("Join(data, \"|\") : %v\n\n", u.Join(data.Value(), "|"))
if u.Join(nums, "|") == u.Join(orig_data, "|") {
fmt.Println("No Side Effects!")
} else {
log.Print(errors.New("WARNING - Side Effects!"))
}
}
Notes - FP
Functional Programming Defined
Functional programming (FP) is a programming paradigm, a style of building the structure and elements of computer programs, that treats computation as the evaluation of mathematical functions and avoids state and mutable data. It is a declarative programming paradigm, which means programming is done with expressions. In functional code, the output value of a function depends only on the arguments that are input to the function, so calling a function f twice with the same value for an argument x will produce the same result f(x) both times. Eliminating side effects, i.e. changes in state that do not depend on the function inputs, can make it much easier to understand and predict the behavior of a program, which is one of the key motivations for the development of functional programming.
In contrast, imperative programming changes state with commands in the source language, the most simple example is the assignment. Functions do exist, not in the mathematical sense, but in the sense of subroutine. They can have side effects that may change the value of program state. Functions without return value therefore make sense. Because of this, they lack referential transparency, i.e. the same language expression can result in different values at different times depending on the state of the executing program.
Functional Programming Features
Separation of Concerns
FP provides for separation-of-concerns by emphasizing function composition, where smaller, simpler functions are combined to build more complicated ones.
In the code snippet below from this example, we combine the _filter and _map functions to perform both actions on the data, that is kept safe in the running function call stack.
When we want the result of all the actions performed by all the chained functions we call the Value method.
data._filter(MEDIUM)._map(strings.ToUpper).Value()
The ability to easily compose functions encourages factoring (breaking apart) functions for maintainability and code reuse.
No State
FP brings you to the holy grail of modularity: no side effects at all. You just have your f(x) = y.
Put x in, get y out. No changes to x or anything else.
FP makes you stop thinking about state, and start thinking in terms of values.
All of your functions simply receive values and produce new values.
FP does not need to protect the shared state of a variable, because the value is fixed.
This in turn avoids the majority of the hoop jumping that imperative languages go through to implement an algorithm across processors or machines to protect state.
Having no state means that your application is modular.
Modularity
There are no dependencies where one part/object of your app mutates a piece of data that is used by another part/object of your app, which then creates an inconsistent state (and bugs).
In FP, rather than mutating the state of objects, we simply return a new object with the changes we want.
FP makes heavy use of stacks and recursion.
Lazy Evaluation
Lazy evaluation improves application performance.
It allows you to define a variable and only load it in memory and only use it when needed.
Golang provides closures when and be leveraged to perform lazy evaluation, but we don't cover that in this example.
Sync's can be included to avoid re-evalution.
Futures can be used to compute a value before you need to actually use the value.
Lazy evaluation of function parameters can be emulated using Go channels and goroutines to implement high-level thunks, but again we don't cover that in this example.
Closures
Go supports closures via anonymous functions.
Anonymous functions are useful when you want to define a function inline without having to name it.
A closure is associated with the environment when they are defined.
Full closures can ensure referential transparency.
The following example comes from Go by Example: Closures
package main
import "fmt"
// intSeq returns another function, which we define anonymously in the body of intSeq.
// The returned function closes over the variable i to form a closure.
func intSeq() func() int {
i := 0
return func() int { // anonymous function
i += 1
return i
}
}
func main() {
// We call intSeq, assigning the result (a function) to nextInt.
// This function value captures its own i value,
/// which will be updated each time we call nextInt.
nextInt := intSeq()
fmt.Println(nextInt()) // 1
fmt.Println(nextInt()) // 2
fmt.Println(nextInt()) // 3
// Create a new closure to prove that state is unique to that particular function.
newInts := intSeq()
fmt.Println(newInts()) // 1
}
$ go run closures.go
1
2
3
1
Recursion
Go supports recursive functions. Here’s a classic factorial example.
The following example comes from Go by Example: Recursion
package main
import "fmt"
//This fact function calls itself until it reaches the base case of fact(0).
func fact(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1)
}
func main() {
fmt.Println(fact(7))
}
$ go run recursion.go
5040
Execution Order
Implicit programming is procedural and uses state for synchronization.
_filter(MEDIUM); _map(strings.ToUpper)
In this example, _filter runs before _map; Possibly because _filter sets up some data that _map needs.
FP uses chaining of function calls:
data._filter(MEDIUM)._map(strings.ToUpper)
In stateless programming, the order of execution matters only if the output of the first function called is needed as an input for the subsequent function.
In our example, the order does not matter, other than in CPU cycles required.
data = ChainLink{nums}
fmt.Printf("filtered and mapped (MEDIUM sized words): %#v\n",
data._filter(MEDIUM)._map(strings.ToUpper).Value())
data = ChainLink{nums}
fmt.Printf("mapped and filtered (MEDIUM sized words): %#v\n",
data._map(strings.ToUpper)._filter(MEDIUM).Value())
// OUTPUT (same for both)
// filtered and mapped (MEDIUM sized words): []string{"TINY", "MARATHON", "PHILANTHROPINIST"}
Referential Transparency
In FP, referential transparency means that given a function and an input value, you will always receive the same output.
There is no external state used in the function.
_filter is an example of a referentially transparent function:
filtered := data._filter(MEDIUM)
fmt.Printf("filtered (MEDIUM sized words): %#v\n", filtered)
fmt.Printf("filtered and mapped (MEDIUM sized words): %#v\n",
filtered._map(strings.ToUpper).Value())
data = ChainLink{nums}
fmt.Printf("filtered and mapped (MEDIUM sized words): %#v\n",
data._filter(MEDIUM)._map(strings.ToUpper).Value())
// OUTPUT (same for both)
// filtered and mapped (MEDIUM sized words): []string{"TINY", "MARATHON", "PHILANTHROPINIST"}
Given referential transparency, we can manipulate FP function calls like components in an algebraic expression.
So, we can assign data._filter(MEDIUM)
to the variable filtered and either use that variable or explicitly execute data._filter(MEDIUM)
and get the same results each time.
No ReAssignment Statements
When a variable X is created, it is given an initial value Y; Variable X is never subsequently modified.
So, in FP, we can be sure that each occurrence of X denotes the same value of Y.
Higher Order Functions
First-class functions are first-class citizens that can be:
- named by a variable
- used as parameter
- returned as a result
- stored in any kind of data structure
Currying allows functions to yield new functions as their return value.
Functional Programming in Go
Go supports first class functions, higher-order functions, user-defined function types, function literals, closures, and multiple return values.
Go allows you to choose to implement your solutions using functional programming techniques.
Go does not go out if its way to encourage you to do so, but as you can see in this example, it's not difficult.
If you prefer the usual functional ways to work with sequences of data in Go, there are 3rd party libraries for that like:
High Order Functions and Chaining in this Example
A Golang first-class function, stringFunc, is used as input to the _filter method, which uses that function's logic to perform it's filtering of data.
Each method (_map, _filter) is linked to a ChainLink struct.
The ChainLink struct has a single field, Data, whose value can be modified as the chain of calls progress.
The ChainLink struct has one method, Value, that is used to extract the current value of it's Data field.
Notes - Enum
This example also demonstrates the following usage of Go enums:
const (
TEENINY WordSize = 1
SMALL WordSize = 4 << iota
MEDIUM // assigned 8 from iota
LARGE // assigned 16 from iota
XLARGE WordSize = 32000
)
An enum consisting of constants (TEENINY, SMALL, MEDIUM, LARGE, XLARGE) is created to indicate the number of chararacters for that type of word, e.g., a SMALL word has 4 or fewer characters.
The WordSize type is used in the definition of the constants.
Go's iota identifier is used in const declarations to very concisely define the related WordSize constants, simplifying the definitions of those incrementing numbers; It is given an initial value of 4. So, each subsequent constant in the enum is incremented by 4.
The ability to attach a method such as String to any user-defined type makes it possible for arbitrary values to format themselves automatically for printing. Although you'll see it most often applied to structs, this technique is also useful for scalar types such as floating-point types like ByteSize.
The String method linked to the WordSize struct causes the text "TEENINY" to be printed instead of the integer 0.
Notes - go-goodies
We use the Join function from go-goodies/goutils to verify that there are no side effects from calling *filter* and _map:
if u.Join(nums, "|") == u.Join(nums_orig_state, "|") {
fmt.Println("No Side Effects!")
. . .
Note: The use of u.Join is not necessarily the most efficient option, but helps to visually illustrate the point.
Output
unfiltered: []string{"tiny", "marathon", "philanthropinist", "supercalifragilisticexpialidocious"}
filtered: &main.ChainLink{Data:[]string{"tiny", "marathon", "philanthropinist"}}
filtered and mapped (MEDIUM sized words): []string{"TINY", "MARATHON", "PHILANTHROPINIST"}
filtered and mapped (MEDIUM sized words): []string{"TINY", "MARATHON", "PHILANTHROPINIST"}
filtered twice and mapped (SMALL sized words): []string{"TINY", "MARATHON"}
mapped and filtered (XLARGE sized words): []string{"TINY", "MARATHON", "PHILANTHROPINIST", "SUPERCALIFRAGILISTICEXPIALIDOCIOUS"}
** Constants ***
SMALL: 8
MEDIUM: 16
LARGE: 32
XLARGE: 32000
TEENINY: TEENINY
Join(nums, "|") : tiny|marathon|philanthropinist|supercalifragilisticexpialidocious
Join(orig_data, "|"): tiny|marathon|philanthropinist|supercalifragilisticexpialidocious
Join(data, "|") : TINY|MARATHON|PHILANTHROPINIST|SUPERCALIFRAGILISTICEXPIALIDOCIOUS
No Side Effects!
Process finished with exit code 0
Conclusion
Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It's a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.
So, every time that I think, "There are no 'objects' in Golang. Where are the objects?" ... or "Where are the 'generics'?" ... or "Where are the built-in functional programming idioms?", Go provides a better way of doing what I want to do.