Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Functional types / non-fully-specified types to assisted Inject #1010

Open
Groostav opened this issue May 7, 2016 · 2 comments

Comments

@Groostav
Copy link

Groostav commented May 7, 2016

As a Kotlin developer, I would like to use assisted inject with a functional type as a factory so that I am able to test my code with less boiler plate

So right now in Kotlin there is no elegant solution for binding kotlin.Function<A, B> as factories.

I thought I could circumvent this with guava's impossibly clever TypeLiteral, no dice.

Given the class

class Component @Inject constructor(@Assisted importantRuntimeValue : Int){
    interface Factory { fun create(importantRuntimeValue : Int) : Component }
}

    //...

class ComponentFactoryConsumer @Inject constructor(val factory : Component.Factory){
    //...
}

class ComponentFactoryTestClass{
  @Test fun when_doing_X_should_result_in_Y(){
    val componentUnderTest = ComponentFactoryConsumer( { importantRV -> mock(Component::class) } )
    //not legal kotlin, since functional types exist in kotlin, SAM conversion is not available here. 
  }
}

The corresponding (almost identical) java has become idiomatic on our project

Since the above kotlin is not legal, as we move forward with kotlin, we need new idioms. Unfortunately the most obvious one requires some modifications within guice.

what I would like to do is skip the named factory type alltogether, and simply use a (Int) -> Component (kotlin alias for kotlin.Function<Int, Component> which itself is an alias for kotlin.jvm.functions.Function<Int, Component>), thus:

class Component @Inject constructor(@Assisted importantRuntimeValue : Int){
  //no need for named factory!
}

    //...

class ComponentFactoryConsumer @Inject constructor(val factory : (Int) -> Component){
    //...
}

class ComponentFactoryTestClass{
  @Test fun when_doing_X_should_result_in_Y(){
    val componentUnderTest = ComponentFactoryConsumer( { importantRV -> mock(Component::class) } )
    //now legal since the lambda is a simple function
  }
}

But this of course requires the binding code:

@Test fun when_trying_to_use_reified_type_and_clever_typeLiteral_should_also_fail(){
    assertThatThrownBy { Guice.createInjector(object : AbstractModule() {
        override fun configure() {
            install(FactoryModuleBuilder().build<(Int) -> KotlinClassWithJavaIdiomaticFactory>())
        }
    }) }.isInstanceOf(CreationException::class.java)
}

inline fun <reified TFactory : Any> FactoryModuleBuilder.build() : Module {
    return build(object : TypeLiteral<TFactory>(){})
}

which results in

May 06, 2016 4:20:08 PM com.google.inject.internal.MessageProcessor visit
INFO: An exception was caught and reported. Message: java.lang.IllegalArgumentException: Expected a Class, ParameterizedType, or GenericArrayType, but <? extends com.empowerops.language.KotlinLanguageFixture$KotlinClassWithJavaIdiomaticFactory> is of type com.google.inject.internal.MoreTypes$WildcardTypeImpl
java.lang.IllegalArgumentException: Expected a Class, ParameterizedType, or GenericArrayType, but <? extends com.empowerops.language.KotlinLanguageFixture$KotlinClassWithJavaIdiomaticFactory> is of type com.google.inject.internal.MoreTypes$WildcardTypeImpl
    at com.google.inject.internal.MoreTypes.getRawType(MoreTypes.java:208)
    at com.google.inject.TypeLiteral.<init>(TypeLiteral.java:90)
    at com.google.inject.TypeLiteral.get(TypeLiteral.java:157)
    at com.google.inject.TypeLiteral.resolve(TypeLiteral.java:181)
    at com.google.inject.TypeLiteral.getReturnType(TypeLiteral.java:339)
    at com.google.inject.assistedinject.FactoryProvider2.<init>(FactoryProvider2.java:239)
    at com.google.inject.assistedinject.FactoryModuleBuilder$1.configure(FactoryModuleBuilder.java:334)
    at com.google.inject.AbstractModule.configure(AbstractModule.java:62)
    at com.google.inject.spi.Elements$RecordingBinder.install(Elements.java:340)
    at com.google.inject.AbstractModule.install(AbstractModule.java:122)
    at com.empowerops.language.KotlinLanguageFixture$when_trying_to_use_reified_type_and_clever_typeLiteral_should_also_fail$1$1.configure(KotlinLanguageFixture.kt:74)
    at com.google.inject.AbstractModule.configure(AbstractModule.java:62)
    at com.google.inject.spi.Elements$RecordingBinder.install(Elements.java:340)
    at com.google.inject.spi.Elements.getElements(Elements.java:110)
    at com.google.inject.internal.InjectorShell$Builder.build(InjectorShell.java:138)
    at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:104)
    at com.google.inject.Guice.createInjector(Guice.java:96)
    at com.google.inject.Guice.createInjector(Guice.java:73)
    at com.google.inject.Guice.createInjector(Guice.java:62)
    at com.empowerops.language.KotlinLanguageFixture$when_trying_to_use_reified_type_and_clever_typeLiteral_should_also_fail$1.call(KotlinLanguageFixture.kt:72)
    at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:61)
    at org.assertj.core.api.StrictAssertions.catchThrowable(StrictAssertions.java:594)
    at org.assertj.core.api.StrictAssertions.assertThatThrownBy(StrictAssertions.java:564)
    at com.empowerops.language.KotlinLanguageFixture.when_trying_to_use_reified_type_and_clever_typeLiteral_should_also_fail(KotlinLanguageFixture.kt:72)

I'm not sure if the problem is the fact that I'm using the type-parametric factory (Int) -> Component (aka kotlin.Function<Int, Component>), or if the problem is within kotlin and its handling of contravariance, but in either event I figured guice should do something other than log a message at level info and then throw that same message as an exception.

The very similar code:

public static class Component{
    @Inject
    public Component(@Assisted Integer value){}
}

@Test
public void when_guice(){
    TypeLiteral<Function<Integer, Component>> factoryInterfaceKey = new TypeLiteral<Function<Integer, Component>>() {};

    AbstractThrowableAssert assertion = assertThatThrownBy(() -> {
        Guice.createInjector(binder -> {
            binder.install(new FactoryModuleBuilder().build(factoryInterfaceKey));
        });
    });

    assertion.isInstanceOf(CreationException.class);
    assertion.hasMessageContaining("Unable to create injector, see the following errors:");
    assertion.hasMessageContaining("1) java.util.function.Function cannot be used as a key; It is not fully specified.");
}
    }

... passes, meaning guice throws a more conventional exception when called from java.

tl;dr, add support for binding functional types as factories

@Groostav
Copy link
Author

Groostav commented May 7, 2016

Worth mentioning, a fairly obvious middle ground would be to create a named type that extends a functional one:

class Component @Inject constructor(@Assisted important : Int){
  interface Factory : (Int) -> Component
}

this has the interesting effect of killing the compiler when you go to use it with a lambda from tests:

  val factory : Component.Factory = { value -> mock(Component::class) }
  //throws an internal error at compilation time

see KT-12217 for more details

@Groostav
Copy link
Author

Groostav commented May 7, 2016

I really just want to use assisted inject from kotlin in any way possible. Right now the only thing I've got working is more no-fun anonymous object overloads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant