Open kdungs opened 1 year ago
A few unsorted thoughts
(T, error)
. The library should acknowledge that. R[T]
without generic member functions isn't useful. Without them, any code using the library ends up building chains of lazy computations anyway. So result
and baresult
are functionally equivalent modulo extra calls to Wrap and Unwrap. With generic member functions eagerness would also make sense again.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:
func(A) B
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.
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
(A -> B, error) -> (B, error -> C, error) -> (A -> C, error)
(B -> C) -> (B, error -> C, error)
(B -> C, error) -> (B, error -> C, error)
or
(A -> B, error) -> (B -> C, error) -> (A -> C, error)
(A -> B, error) -> (B -> C) -> (A -> C, error)
Does this have a name?From this, I conclude that going forward there are really two separate libraries...
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".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...
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?
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.
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.
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...
on the Gopher Slackgive a lightning talk somewhere) to see if this would be interesting and what other feedback / requests people would haveThen
vs.Bind
)R[T]
is even meaningful or if(T, error)
is enough; if we keepR[T]
decide on struct vs interface, see #2.