Brian Slesinsky's Weblog
 
   

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.