25

I want to create a method chaining API in Go. In all examples I can find the chained operations seem always to succeed which I can't guarantee. I therefore try to extend these to add the error return value.

If I do it like this

package main

import "fmt"

type Chain struct {
}

func (v *Chain)funA() (*Chain, error ) {
    fmt.Println("A")
    return v, nil
}
func (v *Chain)funB() (*Chain, error) {
    fmt.Println("B")
    return v, nil
}
func (v *Chain)funC() (*Chain, error) {
    fmt.Println("C")
    return v, nil
}

func main() {
    fmt.Println("Hello, playground")
    c := Chain{}
    d, err := c.funA().funB().funC() // line 24
}

The compiler tells me chain-err-test.go:24: multiple-value c.funA() in single-value context and won't compile. Is there a good way so funcA, funcB and funcC can report an error and stop that chain?

johannes
  • 15,807
  • 3
  • 44
  • 57
  • You can use panic but of course this means you'll have to recover on each method or at the root of it. You can also make the `Chain` object stateful with an error and check it for each method. – Not_a_Golfer Dec 04 '14 at 15:32
  • @Not_a_Golfer true, but I'm wondering whether there's a good idiomatic way for doing this. In my world error conditions are everywhere and in some places (i.e. operators as in http://www.golangpatterns.info/object-oriented/operators ) such chaining should give a nice API. (the first example on that page i.e. isn't a nice API - either you have tons of functions for all types or a switch where wrong types aren't detected by the compiler) – johannes Dec 04 '14 at 15:41
  • 1
    I remember a nicer API involving functions returning functions, as in http://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis (done after http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html). It is not exactly "chaining" though. – VonC Dec 04 '14 at 16:05
  • @johannes I did something similar but used channels to chain output streams, and an error just closes the channel, winding down the entire chain. It's probably not what you need though. – Not_a_Golfer Dec 04 '14 at 16:06
  • 1
    I know this isn't the answer you're looking for, but the idiomatic thing is to avoid chaining. There's nothing wrong with chaining, but it's a consequence of the idiom of using error values instead of exceptions. – weberc2 Dec 04 '14 at 17:29
  • @weberc2 well if there is no good way then your answer below might be a good one, my hope was that I've overseen a thing being quite unexperienced in Go. – johannes Dec 04 '14 at 17:53
  • @johannes I think that's what you'll find. Go places language simplicity very highly, and "clever" workarounds to impose other idioms are often difficult, even when the result is less boilerplate (and rightly so; idiomatic boilerplate is still much more readable than a "clever", non-idiomatic solution). Again, this doesn't mean method chaining is bad practice--it just isn't well suited to Go. – weberc2 Dec 04 '14 at 18:44

6 Answers6

43

Is there a good way so funcA, funcB and funcC can report an error and stop that chain?

Unfortunately, no, there is no good solution to your problem. Workarounds are sufficiently complex (adding in error channels, etc) that the cost exceeds the gain.

Method chaining isn't an idiom in Go (at least not for methods that can possibly error). This isn't because there is anything particularly wrong with method chains, but a consequence of the idiom of returning errors instead of panicking. The other answers are workarounds, but none are idiomatic.

Can I ask, is it not idiomatic to chain methods in Go because of the consequence of returning error as we do in Go, or is it more generally a consequence of having multiple method returns?

Good question, but it's not because Go supports multiple returns. Python supports multiple returns, and Java can too via a Tuple<T1, T2> class; method chains are common in both languages. The reason these languages can get away with it is because they idiomatically communicate errors via exceptions. Exceptions stop the method chain immediately and jump to the relevant exception handler. This is the behavior the Go developers were specifically trying to avoid by choosing to return errors instead.

weberc2
  • 7,423
  • 4
  • 41
  • 57
  • Nice answer (+1). Can I ask, is it not idiomatic to chain methods in Go because of the consequence of returning error as we do in Go, or is it more generally a consequence of having multiple method returns? – user1503949 Dec 04 '14 at 19:57
  • 2
    Thanks for this; it's a good clarification. If only I could upvote twice ;) – user1503949 Dec 04 '14 at 22:16
  • To add to your answer to @user1503949's question - what if you could have multiple receivers for a method in Go? You could then include errors in method chains, right? If that's the case, I'd argue it's the lack of this feature that is the reason chaining isn't idiomatic. Including errors in a hypothetical multiple-receiver Go would require a succeeding method to handle the error of the preceding method, which in my opinion, for chains in Go, just makes sense. (Edit: that said I'm sure this would break the language in other ways) – Eric Dubé Oct 09 '17 at 17:15
  • @EricDubé I don't think multi-receivers makes sense; otherwise you'll be calling functions like this: `(foo, nil).Bar()` just to get method chaining. The real solution to this problem (insofar as it's a problem) is monads. Define a short-circuit-on-error-monad and chain like so: `foo >>= bar >>= baz` (where bar takes a FooResult, and baz takes a BarResult), but this requires a more sophisticated type system for which there are pros and cons (notably a steeper learning curve and less-consistency across programs, not to mention a better-optimizing compiler to maintain performance). – weberc2 Oct 09 '17 at 17:53
  • What's wrong with (foo, nil)? Or, rather, (foo, error(nil))? You'd only have to do it preceding the first method in the chain, which IMO would be a good thing since it makes it clear that chaining is happening. – Eric Dubé Nov 07 '17 at 04:16
11

You can try like that: https://play.golang.org/p/dVn_DGWt1p_H

package main

import (
    "errors"
    "fmt"
)

type Chain struct {
    err error
}

func (v *Chain) funA() *Chain {
    if v.err != nil {
        return v
    }
    fmt.Println("A")
    return v
}
func (v *Chain) funB() *Chain {
    if v.err != nil {
        return v
    }
    v.err = errors.New("error at funB")
    fmt.Println("B")
    return v
}
func (v *Chain) funC() *Chain {
    if v.err != nil {
        return v
    }
    fmt.Println("C")
    return v
}

func main() {
    c := Chain{}
    d := c.funA().funB().funC() 
    fmt.Println(d.err)
}
Kiura
  • 202
  • 3
  • 9
3

If you have control over the code and the function signature is identical you can write something like:

func ChainCall(fns ...func() (*Chain, error)) (err error) {
    for _, fn := range fns {
        if _, err = fn(); err != nil {
            break
        }
    }
    return
}

playground

OneOfOne
  • 95,033
  • 20
  • 184
  • 185
  • Why bother returning the original `*Chain` object? You're not doing anything with it. Even still, this is a clever solution, but it's the kind of clever that is less readable than the writing `if err := funX(); err != nil { /* do something */ }` for X={A, B, C} respectively. – weberc2 Dec 04 '14 at 17:45
  • @weberc2 Mainly because I was too lazy to rewrite the code and wanted to show an example that'd work with his current code ;) – OneOfOne Dec 04 '14 at 18:06
  • 1
    Actually that's brilliant! *Chain could be a reference to some object that's being manipulated, forcing all the functions to always operate on the same thing. Where this falls short is that it's harder to give each function different parameters. Passing a bunch of anonymous functions that call the real functions could work, but it would be incredibly verbose I think. – Eric Dubé Jun 13 '18 at 02:59
1

You can make your chain lazy by collecting a slice of funtions

package main

import (
    "fmt"
)

type (
    chainFunc func() error
    funcsChain struct {
        funcs []chainFunc
    }
)

func Chain() funcsChain {
    return funcsChain{}
}

func (chain funcsChain) Say(s string) funcsChain {
    f := func() error {
        fmt.Println(s)

        return nil
    }

    return funcsChain{append(chain.funcs, f)}
}


func (chain funcsChain) TryToSay(s string) funcsChain {
    f := func() error {
        return fmt.Errorf("don't speek golish")
    }

    return funcsChain{append(chain.funcs, f)}
}

func (chain funcsChain) Execute() (i int, err error) {
    for i, f := range chain.funcs {
        if err := f(); err != nil {
            return i, err
        }
    }

    return -1, nil
}

func main() {
    i, err := Chain().
        Say("Hello, playground").
        TryToSay("go cannot into chains").
        Execute()

    fmt.Printf("i: %d, err: %s", i, err)
}
0

You don't actually need channels and/or contexts to get something like this to work. I think this implementation meets all your requirements but needless to say, this leaves a sour taste. Go is not a functional language and it's best not to treat it as such.

package main

import (
    "errors"
    "fmt"
    "strconv"
)

type Res[T any] struct {
    Val  T
    Halt bool
    Err  error
}

// executes arguments until a halting signal is detected
func (r *Res[T]) Chain(args ...func() *Res[T]) *Res[T] {
    temp := r
    for _, f := range args {
        if temp = f(); temp.Halt {
            break
        }
    }

    return temp
}

// example function, converts any type -> string -> int -> string
func (r *Res[T]) funA() *Res[string] {
    s := fmt.Sprint(r.Val)
    i, err := strconv.Atoi(s)
    if err != nil {
        r.Err = fmt.Errorf("wrapping error: %w", err)
    }
    fmt.Println("the function down the pipe is forced to work with Res[string]")

    return &Res[string]{Val: strconv.Itoa(i), Err: r.Err}
}

func (r *Res[T]) funB() *Res[T] {
    prev := errors.Unwrap(r.Err)
    fmt.Printf("unwrapped error: %v\n", prev)

    // signal a halt if something is wrong
    if prev != nil {
        r.Halt = true
    }
    return r
}

func (r *Res[T]) funC() *Res[T] {
    fmt.Println("this one never gets executed...")
    return r
}

func (r *Res[T]) funD() *Res[T] {
    fmt.Println("...but this one does")
    return r
}

func funE() *Res[string] {
    fmt.Println("Chain can even take non-methods, but beware of nil returns")
    return nil
}

func main() {
    r := Res[string]{}
    r.Chain(r.funA, r.funB, r.funC).funD().Chain(funE).funC() // ... and so on
}
raianmr
  • 71
  • 3
  • 4
-1

How about this approach: Create a struct that delegates Chain and error, and return it instead of two values. e.g.:

package main

import "fmt"

type Chain struct {
}

type ChainAndError struct {
    *Chain
    error
}

func (v *Chain)funA() ChainAndError {
    fmt.Println("A")
    return ChainAndError{v, nil}
}

func (v *Chain)funB() ChainAndError {
    fmt.Println("B")
    return ChainAndError{v, nil}
}

func (v *Chain)funC() ChainAndError {
    fmt.Println("C")
    return ChainAndError{v, nil}
}

func main() {
    fmt.Println("Hello, playground")
    c := Chain{}
    result := c.funA().funB().funC() // line 24
    fmt.Println(result.error)
}
Not_a_Golfer
  • 47,012
  • 14
  • 126
  • 92
  • 2
    An error here won't stop the chain from executing, per the original question. Even worse, the only error here that counts is that of `funC()`; the errors returned by `funA()` and `funB()` are ignored. – weberc2 Dec 04 '14 at 17:40
  • 1
    How about piling up the errors in a slice for investigation-purpose, and in anyway, regard the result as erroneous as the slice has 1 or more elements? – Bert Verhees Sep 07 '17 at 10:34