kdungs / go-result

Result types for Go; because (T, error) can be considered a ✨ monad ✨.
MIT License
1 stars 0 forks source link

Feasibility of turning this into a production quality library #7

Open kdungs opened 1 year ago

kdungs commented 1 year ago

Going from a fun little experiment to a production quality library is not a small increment and shouldn't be taken lightly. Before investing time to build something nobody uses, that solves the wrong problem, or solves it the wrong way...

kdungs commented 1 year ago

A few unsorted thoughts

kdungs commented 1 year ago
kdungs commented 1 year ago

Starting from the conclusion that R[T] without generic member functions is not ideal, I focused a bit more on baresult and what it entails. As stated above, without facilities for composition, there's no real value in having Fmap et al. on their own. Also, knowing that (T, error) is technically also an Applicative is a flex at best. There isn't really practical value in our definition of Apply as far as I can see. When was the last time you called a function that returned a function and an error? But maybe I'm wrong ¯\(ツ)\/¯.

More to the point:

If we ignore higher arities for a moment, there are only two kinds of function signatures that emerge organically in Go:

  1. func(A) B

  2. func(A) (B, error)

    Additionally, there's a third kind that is only relevant in the context of this library; kind of as an implicit intermediate representation.

  3. func(A, error) (B, error)

    It's important to note that #3 is fully defined in terms of #1 and #2 considering Fmap and Bind: Fmap(#1) <=> Bind(#2) <=> #3.

    Ultimately, what we are interested in is building chains of computations through function composition. We want to enable library users to chain together functions without having to write if err != nil a lot. Again, ignoring higher arities for the moment, the result of a composition is always a function with signature func(A) (B, error). The actual chain might look like

(A -> T1, error) . (T1, error -> T2, error) . … . (Tn, error -> B, error)

which is equivalent to

 (A -> T1, error) >=> (T1 -> T2, error) >=> … >=> (Tn -> B, error)

Where . denotes normal composition and >=> denotes Kleisli composition.

So in the end we really only need either

or

From this, I conclude that going forward there are really two separate libraries...

  1. R[T] with eagerness. Either once generic member functions are a thing or through free functions that take the R[T] as their first parameter. This is what I should have done in the first place. The whole lazyness was a nice detour and yielded the entire thought process around (T, error) and composition but when it comes to the usefulness of a dedicated type R[T] it's probably in "here I have a result type, I want to call this function on it if there's a value inside".
  2. A library that deals with the aforementioned compositions. Maybe also stuff for higher arities, let's see... Maybe then would be a nice name for that library... Is it taken, already?

Another, unrelated, thought that I had is that in Go we handle resource cleanup via defer. I.e. there are no destructors. This probably doesn't play very nice with the whole idea of the library...

kdungs commented 1 year ago

Just noting this here to follow up later... One thing that isn't clear to me about constraints is how to express that one type parameter implements the interface defined by another. This is relevant because given

func Chain[A, B, C any](f func(A) (B, error), g func(B) (C, error)) func(A) (C, error) {
    return func(a A) (C, error) {
        b, err := f(a)
        if err != nil {
            return *new(C), err
        }
        return g(b)
    }
}

the following will fail to compile

pipeline := Chain(os.Open, io.ReadAll)

because

type func(r io.Reader) ([]byte, error) of io.ReadAll does not match inferred type func(*os.File) (C, error) for func(B) (C, error)compilerCannotInferTypeArgs

because the return type of os.Open is *os.File which implements io.Reader but of course B cannot both be *os.File and io.Reader.

What works of course is using a helper function

func asReader(f *os.File) io.Reader {
    return f
}

but that seems a bit... unelegant?

kdungs commented 1 year ago

There's now https://github.com/kdungs/go-then as an example of what the lazy composition library could look like. So this library can focus more on the result type and eager functions that act on it.

As a side note: I thought about putting both in the same repo (with multiple modules) but found that it's generally advised against.

kdungs commented 1 year ago

Decided to move the then package back into this repo. Having them together might not be the most idiomatic thing but then again they are experiments and it's nice to have one overarching readme.