I assume that by "perform mappings" you're wondering how to write a function like this:
// Map returns a future that yields fn(r) where r is the result value of f.
func Map[A, B any](f *F[A], fn func(A) B) *F[B]
I don't believe that's possible with your existing package. The reason is that there's no flexibility for additional behaviour in your *F type. It can hold exactly one value of a given type. Asking for the result will yield exactly that value.
But in the above case, you don't just want to yield the value - you want to call a function on that value and return that. There's no way for the *F[B] value to somehow hold the F[A] value inside it, along with its associated type A. However, that restriction only applies when F is a purely static type. You can use a generic interface type to allow arbitrary behaviour here.
Suppose your future type was defined like this:
type F[T any] interface {
String() string
Err() error
Result() (T, error)
Done() <-chan struct{}
}
Then it becomes straightforward to define the above Map function:
func Map[A, B any](f F[A], fn func(A) B) F[B] {
f1 := &fmap[A, B]{
fn: fn,
}
// Note: go2go bug means we can't use F in the initializer above.
f1.F = f
return f1
}
type fmap[A, B any] struct {
F[A]
fn func(A) B
}
func (f *fmap[A, B]) Result() (B, error) {
a, err := f.F.Result()
if err != nil {
return *new(B), err
}
return f.fn(a), nil
}
Note that I deliberately omitted the SetString and MustResult methods from the interface definition above, the former because it makes it into a mutable value, which isn't great in a concurrent setting, and the latter because it can always be defined in terms of Result. They're both easy enough to implement independently:
func WithName[T any](f F[T], name string) F[T] {
f1 := &namedFuture[T]{
name: name,
}
f1.F = f
return f1
}
type namedFuture[T any] struct {
F[T]
name string
}
func (f *namedFuture[T]) String() string {
return
f.name}
func Must[T any](x T, err error) T {
if err != nil {
panic(err)
}
return x
}
The question to ask here is "what do I want to do with it?"
For example, if you don't need to actually use the result values (as is the case in AsCompletedDelayed) you can define an interface that represents the behaviour that you care about.
For example (I'm not keen on the name, but...):
// Doner represents a generic future type - anything that can be waited for.
type Doner interface {
Done() <-chan struct{}
}
In fact, it's easy to define the whole of AsCompleted and AsCompletedDelayed in terms of the above type, with a small tweak to the Delayed type to avoid the
direct dependency on F.
func AsCompletedDelayed[D Doner](ctx context.Context, initial []D, delayed []Delayed[D]) <-chan D {
I hope you find this helpful.
rog.