capabilities akin to Provider / Lazy from dagger
Is your feature request related to a problem? Please describe.
It's difficult currently to use wire for conditional bindings or to delay instantiation of a bound type.
I'm thinking along the lines of dagger's support for Provider and Lazy.
Describe the solution you'd like
If wire can inject a type A, it would be cool if it could also inject a func() A. (There's probably a better solution, but without generics, I'm not sure what it would be.)
For example, let's say A is expensive to instantiate, and I only want to do so if some condition is true. This isn't right:
type A struct{
// expensive things here...
}
var aSet = wire.NewSet(
wire.Struct(new(A), "*"),
dependsOnA)
var someCondition bool
func dependsOnA(a A) B {
// we always pay the cost of instantiating A.
if someCondition {
// do stuff with A
}
// do other stuff, return a B
}
What one would like is:
func dependsOnA(aProvider func() A) B {
if someCondition {
a := aProvider()
// do stuff with A
}
// ...
}
Describe alternatives you've considered
Two alternatives:
-
Build separate graphs, and use conditions do decide which injector function to use. This isn't great if the dependency graph is complex and there are multiple types that need conditional or lazy instantiation, since configuring the entire graph N different ways is tedious.
-
Generate separate injector functions for each lazy/conditional thing, and use those functions inside higher level provider functions. Following the example above:
func dependsOnA() B {
if someCondition {
a := injectA() // this is a wire gen'd function
// do stuff with A
}
// ...
}
I've used "two injector" strategies in the past (with guice) for bootstrapping, but it's not a great to use use all over the place in an app because you'll have multiple disconnected dependency graphs. This is bad if A needs a B, but B is also needed in a graph elsewhere.
I think some sort of wire analog to Lazy or Provider would also help with #205 and #216
Hey Justin. We've mulled this over in the past, and the solution we've come back to in these cases is to stand up a small type that's a provider, since there's a lot of different strategies for how to handle an expensive object. I'm not sure whether there's any API that would be as clear or clearer than writing the provider directly.
Consider something like:
// Assume constructing A is expensive.
type A struct{
// ...
}
type B struct{}
// AProvider provides an A.
type AProvider struct {
DepForA B
init sync.Once
a A
}
func (ap *AProvider) A() *A {
ap.init.Do(func() {
ap.a = A{ /* ... */ }
})
return &ap.a
}
var aSet = wire.NewSet(
wire.Struct(new(B), "*"),
wire.Struct(new(AProvider), "DepForA"),
dependsOnA)
var someCondition bool
func dependsOnA(ap *AProvider) B {
// we always pay the cost of instantiating A.
if someCondition {
// do stuff with ap.A()
}
// do other stuff, return a B
}
(If and when we address #7, it would be trivial to make *AProvider.A() be part of the dependency graph for functions that need it.)
Thanks Ross, that makes a lot of sense. It seems to me this potentially could also lead to support for singleton vs not (just remove the Once to make A not a singleton).
In your code above, I do wonder if there may be dragons here:
ap.a = A{ /* ... */ }
Because other parts of the graph may share dependencies with A, but we wouldn't want any of those dependencies to be instantiated unless we're sure that A (or something else) actually wants them. While at the same time we need to respect their singleton-ness.
Having not thought about it too deeply, it seems like you might need to "provider-ize" all the things. Or store instances in a map of some sort.
Anyhow, thanks for the reply and the work on wire! Injectable providers I think would be a great (perhaps even necessary) addition, esp. for larger projects.
func (ap *AProvider) A() *A { ap.init.Do(func() { ap.a = A{ /* ... */ } }) return &ap.a }
I feel that asking the "provider" to implement singleton logic so that "injector" can stay very simple is not using the code-generation capabilities to their advantage. I mean, this forces user to write a lot of boilerplate, which actually could very well be hidden out of sight and auto-generated, if it was responsibility of "injector" to only call the provider once.
type A struct{
// expensive things here...
}
// expensive to call, we want singleton
func provideA() *A {
return &A{ /* ... */ }
}
// in wire.go:
injectA() {
panic(wire.Singleton(provideA))
}
Here, instead of wire.Build which generates "protype"-like behaviour, we generate "singleton-like" behavior, if we could introduce wire.Singleton method.
// in wire_gen.go:
type AHolder struct {
init sync.Once
a *A
}
func (ap *AHolder) a() *A {
ap.init.Do(func() {
ap.a = provideA() /* code-generator inserts provider call(s) here */
})
return &ap.a
}
var singletonA AHolder /* global variable, but not visible or accessible from outside: only access is through injectA() */
func injectA() *A {
return singletonA.a() /* provider initialization runs once, and object does not re-build at every use of injectA() */
}
The boilerplate can be code-generated and out-of-sight, if it was responsibility of injector to only call provider once to implement singleton behavior.
If there was a way for wire.Build to infer that provider is actually requesting singleton treatment, we would not need to create wire.Singleton method. Can we annotate or tag methods to provide this hint?