DEV Community

Cover image for Abstracting Application IO Using Higher Order Functions
Jake Witcher
Jake Witcher

Posted on

Abstracting Application IO Using Higher Order Functions

In a previous blog post I examined how interfaces can be used to abstract away the details of an application's IO creating code that is easier to test and easier to extend. Although interfaces are a very effective tool for abstraction, they are not the only tool available for separating business logic and IO. Higher order functions are another method we can use to separate the details about file systems or databases from our core business logic.

Higher order functions are functions that rely on other functions as either a parameter type or a return type. Functions like map and filter in Java, Python and many other languages fit the definition of a higher order function. Both of these functions accept another function as an argument; one that either describes how the elements of a collection should be filtered or how the elements should be transformed.

nums = [1, 2, 3, 4, 5]

# using higher order functions in Python
squared_nums = map(lambda n: n * n, nums)
even_nums = filter(lambda n: n % 2 == 0, nums)
Enter fullscreen mode Exit fullscreen mode

Or if you have used the http package in Go, you may be familiar with another higher order function, the http.HandleFunc method. This method takes a function that defines how an http request should be handled for a given route as its second argument.

func handleHomePage(writer http.ResponseWriter, _ *http.Request) {
    _, err := fmt.Fprintf(writer, "This is the Home Page")
    if err != nil {
        log.Fatalln(err)
    }
}

func main() {
    http.HandleFunc("/", handleHomePage)
    log.Fatalln(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

We can illustrate how higher order functions are able to hide IO details using the same example from the previous blog post; an application that calculates an order total from a list of line items. We'll start by writing a version of the CalculateOrderTotal function that takes a function as an argument rather than a struct that satisfies the OrderProvider interface.

func CalculateOrderTotal(providerFunc func() []LineItem) float64 {
    lineItems := providerFunc()

    var orderTotal float64
    for _, lineItem := range lineItems {
        orderTotal += lineItem.Price
    }

    return orderTotal
}
Enter fullscreen mode Exit fullscreen mode

When using higher order functions, the type signature of the function is key. The new parameter providerFunc is a function that takes no arguments and returns an array slice of line items. Any function that matches the type signature func() []LineItem can be used as an argument for this version of CalculateOrderTotal.

The implementation details regarding how this function will retrieve and then return a list of line items is kept hidden from CalculateOrderTotal just as it was before when using the OrderProvider interface.

To test out this new version of CalculateOrderTotal, we can define a function that returns a list of line items stored in memory. However instead of just writing a function that returns a predetermined list of line items, we'll create a provider function factory that creates a function with the desired signature func() []LineItem using a list of line items passed as an argument to the factory function.

func InMemoryLineItemsProviderFuncFactory(lineItems []LineItem) func() []LineItem {
    return func() []LineItem {
        return lineItems
    }
}
Enter fullscreen mode Exit fullscreen mode

This implementation may seem unnecessarily complex, and if all we needed was a static list of line items it certainly would be. But the goal is to keep IO details away from CalculateOrderTotal and to have the freedom to swap out those details on the fly. This factory approach will allow us to test the new version of CalculateOrderTotal with different sets of values stored in memory to make sure it still works the same as before.

var testCases = []struct {
    lineItems []LineItem
    expected  float64
}{
    {
        lineItems: []LineItem{
            {Description: "A", Price: 85},
            {Description: "B", Price: 15},
        },
        expected: 100,
    },
    {
        lineItems: []LineItem{
            {Description: "A", Price: 35.25},
            {Description: "B", Price: 95.5},
        },
        expected: 130.75,
    },
}

func TestHigherOrderFunctionCalculateOrderTotal(t *testing.T) {
    for _, test := range testCases {
        providerFunc := InMemoryLineItemsProviderFuncFactory(test.lineItems)

        if actual := CalculateOrderTotal(providerFunc); actual != test.expected {
            t.Fatalf("expected %.2f, actual %.2f", test.expected, actual)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This test suite is nearly identical to the one we used to test the final version of CalculateOrderTotal in the previous blog post. The line items and expected output are the same however instead of encapsulating the line items with an InMemoryOrderProvider struct that implements the OrderProvider interface we are using InMemoryLineItemsProviderFuncFactory to encapsulate the details in a function.

When running the test suite using the new CalculateOrderTotal it produces the same results as the previous version. The new implementation required very few changes to our code and still prevented the details of our IO from leaking into CalculateOrderTotal.

Now lets take a look at how the same function signature can be used to retrieve the line item data from a JSON file.

func FileBasedLineItemsProviderFuncFactory(filepath string) func() []LineItem {
    return func() []LineItem {
        file, err := ioutil.ReadFile(filepath)
        if err != nil {
            log.Fatalf("unable to read file %s", filepath)
        }

        var lineItems []LineItem
        err = json.Unmarshal(file, &lineItems)
        if err != nil {
            log.Fatalf("unable to parse json for file %s", filepath)
        }

        return lineItems
    }
}
Enter fullscreen mode Exit fullscreen mode

The function FileBasedLineItemsProviderFuncFactory is another provider function factory that returns a function with the signature func() []LineItem based on the filepath parameter of the factory function.

func main() {
    providerFunc := FileBasedLineItemsProviderFuncFactory("data/order_items.json")
    orderTotal := CalculateOrderTotal(providerFunc)

    fmt.Printf("Your total comes to %.2f", orderTotal)
}
Enter fullscreen mode Exit fullscreen mode

At this point our higher order function solution is providing more or less the same functionality as the interface solution from the previous blog post. For most programming languages this would be a good place to stop, however Go has a unique language feature that allows us to use function types as receiver arguments for methods opening up an additional means of abstraction using both higher order functions and interfaces together.

A receiver argument determines the type that can invoke a method in Go in the same way that defining a public method in a C# or Java class allows you to invoke the method from an instance of that class type using the receiver.methodName() syntax.

In Go a receiver can be any type, not just a struct. This allows us to attach a method to a function type and then use that method to satisfy the requirements of an interface.

To start off, we'll need to take our function signature func() []LineItem and assign it to a named type.

type ProviderFunc func() []LineItem
Enter fullscreen mode Exit fullscreen mode

With the function type defined, we can now create a GetLineItems method that uses the ProviderFunc type as a receiver.

// the OrderProvider type from the previous blog post for reference
type OrderProvider interface {
    GetLineItems() []LineItem
}

func (f ProviderFunc) GetLineItems() []LineItem {
    return f()
}
Enter fullscreen mode Exit fullscreen mode

The GetLineItems method of the ProviderFunc type returns the results of calling the function f. In other words we are able to satisfy the GetLineItems method of the OrderProvider interface by having the ProviderFunc function type invoke itself.

Before we can use our two factory functions to create ProviderFunc functions that satisfy the OrderProvider interface, we'll need to change their return type from func() []LineItem to ProviderFunc. Nothing needs to be changed in the body of the factories as the functions they return already satisfy the ProviderFunc type. They just have to be explicitly returned as such.

func InMemoryLineItemsProviderFuncFactory(lineItems []LineItem) ProviderFunc {
    // ...function body remains the same as before
}

func FileBasedLineItemsProviderFuncFactory(filepath string) ProviderFunc {
    // ...function body remains the same as before
}
Enter fullscreen mode Exit fullscreen mode

Now the functions returned by both factories satisfy the parameter type of the new CalculateOrderTotal function (a ProviderFunc can still be used as a func() []LinetItem type) as well as the previous version of CalculateOrderTotal with its OrderProvider parameter type. Whether we use the old or new version of CalculateOrderTotal we are still able to switch data sources with relative ease, going from in memory to file based data retrieval without having to rewrite either version of CalculateOrderTotal.

func NewCalculateOrderTotal(providerFunc func() []LineItem) float64 {
    lineItems := providerFunc()

    var orderTotal float64
    for _, lineItem := range lineItems {
        orderTotal += lineItem.Price
    }

    return orderTotal
}

func PrevCalculateOrderTotal(provider OrderProvider) float64 {
    lineItems := provider.GetLineItems()

    var orderTotal float64
    for _, lineItem := range lineItems {
        orderTotal += lineItem.Price
    }

    return orderTotal
}

func main() {
    fileBasedProviderFunc := FileBasedLineItemsProviderFuncFactory("data/order_items.json")

    // using the file based provider function as a 'func() []LineItem' function type
    orderTotal := NewCalculateOrderTotal(fileBasedProviderFunc)
    fmt.Printf("Your total comes to %.2f", orderTotal)

    // using the file based provider function as an 'OrderProvider' interface type
    orderTotal = PrevCalculateOrderTotal(fileBasedProviderFunc)
    fmt.Printf("Your total comes to %.2f", orderTotal)

    lineItems := []LineItem{
        {Description: "Leather Recliner", Price: 2499},
        {Description: "End Table", Price: 249},
    }

    inMemoryProviderFunc := InMemoryLineItemsProviderFuncFactory(lineItems)

    // using the in memory provider function as a 'func() []LineItem' function type
    orderTotal = NewCalculateOrderTotal(inMemoryProviderFunc)
    fmt.Printf("Your total comes to %.2f", orderTotal)

    // using the in memory provider function as an 'OrderProvider' interface type
    orderTotal = PrevCalculateOrderTotal(inMemoryProviderFunc)
    fmt.Printf("Your total comes to %.2f", orderTotal)
}
Enter fullscreen mode Exit fullscreen mode

Determining which method you use β€” a struct satisfying an interface type, a higher order function, or a function satisfying an interface type β€” will depend on details about the problems you're trying to solve, the application you're building, and personal preference. It is outside the scope of this blog post to discuss how those factors might impact your design decisions.

Regardless of which method best suites the requirements of your application, creating separation between business logic and IO through these abstractions will make your code resilient to change and easier to test.

All example code from both blog posts on abstracting application IO can be found here.

Top comments (0)