3

I'm trying to figure out how to set up middlewares, and right now I've got something like:

func applyMiddleware(h *Handle) *Handle {
   return a(b(c(h)))
}

Is there a way to "compose" these functions so I can just pass a list of Handle(s) and it will return the composed function?

1
  • 2
    they must be func(*Handle) *Handle otherwise it is not valid go code. Commented Oct 19, 2019 at 21:46

3 Answers 3

7

use a slice

https://play.golang.org/p/DrEnGkIEbU3

package main

import (
    "fmt"
)

func main() {
    fmt.Println(v(v(v(0))))
    fmt.Println(compose(v, v, v)(0))
}
func v(i int) int {
    return i + 1
}
func compose(manyv ...func(int) int) func(int) int {
    return func(i int) int {
        for _, v := range manyv {
            i = v(i)
        }
        return i
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

I guess this is actually piping and not composing. The code remains the same except that the order of evaluation is right-to-left. The innermost function is executed first and then the outer ones follow. Correct me if I am wrong.
no this is commonly accepted as function composition. Piping is when you connect a data flow producer to a data flow consumer.
1

Since the introduction of generics in Go 1.18, you can define the following generic function for composing two arbitrary functions:

func compose[A any, B any, C any](f func(A) B, g func(B) C) func(A) C {
    return func(a A) C {
        return g(f(a))
    }
}

The order of composition of compose above is left to right, i.e., compose(f, g) returns the function "g after f" or gf, equivalent to g . f in Haskell and to f >> g in F# or Elm.

In other words, the input to the resulting function – compose(f, g) – is fed into f, then f's output is fed into g, whose output is the final result:

order of composition of compose(f, g)


Let's first define the type Adapter for representing the type of functions you want to compose:

type Adapter = func(*Handle) *Handle

With compose and Adapter, you can now define composeAdapters for composing an arbitrary number of these Adapters functions:

func composeAdapters(adapters ...Adapter) Adapter {
    composition := func(h *Handle) *Handle {
        return h
    }
    for _, adapter := range adapters {
        composition = compose(composition, adapter)
    }
    return composition
}

Note that composition is initialized to the identity function for *Handle. You can think of it as a no-op adapter: it just forwards the input to the resulting composed adapters to the first function to compose in the chain, if any. This also implies that calling composeAdapters without any arguments – e.g., composeAdapters() – results in the no-op adapter: it performs no action on the input *Handle; it just gives it back.

Given the functions f, g, and h of type Adapter – i.e., func(*Handle) *HandleapplyMiddleware can be implemented as:

var applyMiddleware = composeAdapters(f, g, h)

Note again the order of composition:

order of composition of applyMiddleware

Comments

0

JFMR's response is the most direct. However, for folks who like a functional-programming slant ... you could alternatively compose functions by creating a Functor interface.

// functor via embedding
type Functor[A any, B any, C any] interface {
    fmap(func(A) B) C
}

// holding the "function" type in a way that is compatible with Functor
type Wtf[R any, A any, B any] func(R) A

// implementation/instance of Functor for anonymous functions!
func (g Wtf[R, A, B]) fmap(f func(A) B) Wtf[R, B, A] {
    h := func(t R) B { return f(g(t)) }
    var funout Wtf[R, B, A] = h
    return funout
}

In the implementation, the return types was purposely chosen to be the embedded type, Wtf[R,B,A], rather than fn(R)B in order to permit the chaining of composition operations.

Then to show how it works ...

func main(){
    var f1 Wtf[int, int, int] = func(t int) int { return 1 * t }
    f2 := func(t int) int { return 2 * t }
    f3 := func(t int) int { return 3 * t }
    fmt.Println((f1.fmap(f2).fmap(f3))(7)) // 42 
}

Please notice that ordering of the functions is the same that you'd see when using piping operations in other languages (like f1 |> f2 |> f3).

The nice thing about this construction is that not only does it allow you to chain operations together, but you can create other implementations of the interface for other data-types. For example, when applied to collections, it gives you the behavior that you'd expect to see from the functional-programming map-function (not to be confused with Go's function of the same name).

A nice thing about this construction is that the only constraint on types is that they're consistent with permitting composition.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.