Sunday, 17 Feb 2008
Guice callbacks should take parameters
Guice is easy to use when you want to
build a web of objects based
on a static configuration. However, it gets awkward when you want to create multiple, similar webs
of objects, where only a few nodes vary.
This is because Guice has no direct support for callbacks that take parameters. The only callback
that's built into Guice is Provider.get().
I would like to see full support for callbacks with parameters in the next version of Guice. In
this article, I'll sketch out how we could do it.
The Basic Idea
Suppose that some bindings in an injector could be defined as slots to be filled in later.
For example, here's an injector with four bindings:
i1 = createInjector(A -> A(),
B -> B(A),
C -> ?,
D -> D(C))
By this shorthand, I mean that A has a zero-argument constructor, B has a constructor that takes an
instance of A as an argument, C is bound to a slot (that is, undefined), and D has a constructor
taking an instance of C. This would result in an injector with the following bindings:
- i1.getInstance(A.class) means: new A()
- i1.getInstance(B.class) means: new B(new A())
- i1.getInstance(C.class) cannot be evaluated,
so it throws a "C is undefined" exception
- i1.getInstance(D.class) also throws "C is undefined".
Now, suppose the injector has a bind() method that we can use to give C a value:
i2 = i1.bind(C.class, myC)
This constructs a new injector, i2, that shares all bindings with the original, except that it fills
in the slot for C with a value. This defines D as well:
- i2.getInstance(C.class) means: myC
- i2.getInstance(D.class) means: new D(myC)
Hiding the Injector
We wouldn't want to call bind() directly in application code because that would require
injecting the injector. (If
inject is the new
import, injecting the injector is the new wildcard import.)
We could avoid this by adding a new callback interface to Guice, similar to Provider:
interface Function<P,V>() {
V call(P parameter);
}
Then we can request a callback from application code:
@Inject
Function<C,D> getD;
If we are in the context of i1, here's how a function call to getD simplifies:
- getD.call(myC)
- => i1.bind(C.class, myC).getInstance(D.class)
- => new D(myC)
Going Deeper
So far this is much like
assisted
inject. However, assisted inject only supports shallow parameter injection; it
constructs factories that pass parameters to the root object of the new web of objects. But if
functions are built into the Guice core, we should be able to pass parameters anywhere. For example:
i3 = createInjector(A -> ?,
B -> B(A),
C -> C(A),
D -> D(B,C),
E -> E(D));
@Inject
Function<A,E> getE;
Here's how getE simplifies:
- getE.call(myA)
- => i1.bind(A.class, myA).getInstance(E.class)
- => new E(new D(new B(myA), new C(myA)))
It's possible to implement function injection without modifying Guice itself, by using a
thread-local scope. I implemented a
prototype that I believe handles the previous examples. However, suppose
you inject a function that, when called, builds another function? Then you get... closures!
Here's a simple example of a curried function:
i4 = createInjector(A -> ?, B -> ?, C -> C(A, B))
@Inject
Function<A,Function<B,C>> getC;
Here's how passing one argument simplifies:
- getC.call(myA)
- => i2.bind(A.class, myA).getFunction(B.class, C.class)
- => f(x) -> new C(myA, x)
If we pass both arguments:
- getC.call(myA).call(myB)
- => i2.bind(A.class, myA).getFunction(B.class, C.class).call(myB)
- => new C(myA, myB)
In a real application, closures will be much less obvious. The return
value of Function.call() can be a complex web of objects that can contain a Provider
or Function callback anywhere in it. There's nothing preventing any of these callbacks from
depending on the value passed in the outer function call.
This is where thread-local scopes break down. An inner function or provider will be called
after the outer function has returned and the parameter has gone out of scope. It could also
be called from any thread. To make parameter bindings work regardless of when an inner callback is
called, Guice has to capture the argument to the outer function and make it permanently available to
each inner callback that has a dependency on it.
Closures may seem complicated to implement, but I believe they fit better with Guice than
thread-local scopes. Thread-local scopes have both a beginning and an end, while closures only have
a beginning; they live indefinitely until garbage-collected. Since Guice is really about creating
objects, not managing their lifecycles, this would get rid of a lot of sticky issues that arise when
we want to do error-checking for scopes.
(On the other hand, thread-locals do fit very well with web apps because HTTP requests and responses
should not live after the request has been handled. We would need some other mechanism to handle
cleanup.)
Improving Guice's callback interface
In theory, only a single-argument Function interface is necessary, because we can always use
curried functions to simulate multiple arguments. But for usability, it would be much nicer to have
direct support for multiple-argument functions. One way would be to define interfaces for
Function2, Function3, and so on. This is a little awkward but certainly liveable. (Of course, if
someday Java gets real function types, we could use those instead.)
Alternately, we could borrow from assisted inject the idea of callbacks that implement an interface
of the developer's choice, which might look something like this:
@Factory
interface FooFactory {
Foo make(Bar bar, Baz baz);
}
@Inject
FooFactory fooFactory;
We should be able to implement FooFactory.make() using any injector where Bar and
Baz are bound as slots and Foo depends on no other slots. The implementation
is just the function that naturally arises from following Foo's dependencies in the injector.
Callback interfaces could just as easily have multiple methods:
@Factory
interface MyFactory {
Foo getFoo();
Bar getBar(Param param);
Baz getBaz(Param param, Param2 param2);
...
}
This flexibility could make Guice useful in more situations. There is no longer any reason that
Guice has to define specific callback interfaces such as Provider and Function.
It could implement whatever callback interface you like.
respond | link |
/code
|