Functional Composition

Static composition using functions

Go does not provide object inheritance and instead puts the focus on composition. This is usually discussed in terms of embedding types, however there are other ways to compose functionality.

Functional Composition is applying one function to the result of another to build up aggregated behavior. Doing this in Go is particularly powerful because functions are first class objects that can be returned, and function definitions can create closures over variables they reference.

The most common example of this style of composition in Go is HTTP handlers. The following example returns a http.HandlerFunc that wraps a function offering REST services with JSON serialisation of output and error handling:

type RestAPI func(w http.ResponseWriter, r *http.Request) (interface{}, error)

func jsonHandler(handler RestAPI) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var jresult []byte
        result, err := handler(w, r)
        w.Header().Set("Content-Type", "application/json")

        if err != nil {
            // Marshalling into a specific JSON error object can be done here
            jresult, jerr := json.Marshal(err)
            if jerr == nil {
                w.Write(jresult)
            } else {
                // Internal error handling
            }
            return
        }

        if result != nil {
            jresult, err = json.Marshal(result)
            if err != nil {
                // Internal error handling.
                return
            }
        }
        w.Write(jresult)
    }
}

Rest services now only have to deal with reading HTTP input and returning objects suitable for serialisation to JSON:

func helloWorld(w http.ResponseWriter, r *http.Request) (interface{}, error) {
    return struct {
        Message string
    }{
        "Hello World",
    }, nil
}

These are then brought together when we register URLs:

http.HandleFunc("/hello", jsonHandler(helloWorld))

Additional functionality can be composed in by defining functions that wrap a RestAPI and return their own RestAPI:

func logApiUse(name string, api RestAPI) RestAPI {
    return func(w http.ResponseWriter, r *http.Request) (interface{}, error) {
        start := time.Now()
        result, resultErr := api(w, r)
        end := time.Now()
        fmt.Printf("%v: %s completed in %v\n", start, name, end.Sub(start))
        return result, resultErr
    }
}

Composing these together is again done at URL registration time:

http.HandleFunc("/test", jsonHandler(logApiUse("/test - Hello World", helloWorld)))

Micro languages

Functional composition can also be used to create micro languages that enable complex behaviour to be built up. For example, parsing a CSV file results in a series of []string records. If we wish to filter out particular records of interest we could write lots of nested if statements, but the logic can become complex and error prone.

As an alternative we can create a series of small composable functions that allow the logic of the filtering to be constructed more clearly:

type Filter func(record []string) bool

func Equal(column int, value string) Filter {
    return func(record []string) bool {
        if len(record) <= column {
            return false
        }
        if record[column] == value {
            return true
        }
        return false
    }
}

The Equal function returns a Filter function that takes []string records and returns true if the given column matches the specified value. And and Or functions can be created that string filters together:

func And(filters ...Filter) Filter {
    return func(record []string) bool {
        for _, f := range filters {
            if !f(record) {
                return false
            }
        }
        return true
    }
}

func Or(filters ...Filter) Filter {
    return func(record []string) bool {
        for _, f := range filters {
            if f(record) {
                return true
            }
        }
        return false
    }
}

These allow us to build moderately complex filtering logic in a readable and easily extendable way:

testRecord := []string{"apple", "pear", "orange", "bear"}
query1 := And(Equal(0, "apple"), Equal(1, "lemon"))
query2 := Or(query1, And(Equal(2, "orange"), Equal(3, "bear")))

if query1(testRecord) {
    fmt.Printf("Query 1 will not match.\n")
}

if query2(testRecord) {
    fmt.Printf("Query 2 matched.\n")
}

Additional functions that return Filters can be created and composed in as required (e.g. comparing against numeric or date values, mapping data from one value to another).

Last Modified: Sat, 23 Jan 2016 16:12:17 GMT

Made with PubTal 3.5

Copyright 2017 Colin Stewart

Email: colin at owlfish.com