Open gonciarz opened 1 year ago
/cc @manovotn, @mkouba
I'm not a kotlin expert but I wonder how does kotlin translate the typealiases for AssignKeyboard
and AssignMouse
? I would expect something like Function<Computer, Either<KeyboardError, Unit>>
but that's obviously not the case because then you would not get an AmbiguousResolutionException
.
BTW an idiomatic way in CDI is to use a special qualifier per each bean that has a shared type in its set of bean types; i.e. something like @Keyboard
and @Mouse
in this particular case.
@mkouba that's a bit different bean:
kotlin.jvm.functions.Function2<? super rg.zerokvm.domain.model.Computer, ? super kotlin.coroutines.Continuation<? super arrow.core.Either<? extends rg.zerokvm.domain.port.api.BroadcastSwitchActiveComputerError, ? extends kotlin.Unit>>, ?>
They key thing in that definition is a suspended function, that introduces co-routines and state machine under the hood. So the second argument is translated into Continuation.
Having an alias for function simplifies the code, but it's just kotlin's function (Function2). I guess that the code that scans bean should be aware that this is generic type should register a bean using TypeLiteral. And the same for searching the right bean.
This snippet works perfectly:
val typeLiteral = object : TypeLiteral<AssignMouse>() {}
val beanInstance = CDI.current().select(typeLiteral)
They key thing in that definition is a suspended function, that introduces co-routines and state machine under the hood. So the second argument is translated into Continuation.
I see. But this shouldn't be a problem if kotlin translates the typealias consistently (i.e. when used as a return type or when used a constructor parameter). Is BroadcastSwitchActiveComputerError
extended by KeyboardError
and MouseError
? I would expect something like Function2<? super Computer, ? super Continuation<? super Either<? extends KeyboardError, ? extends Unit>>, ?>
instead...
@mkouba
I see. But this shouldn't be a problem if kotlin translates the typealias consistently (i.e. when used as a return type or when used a constructor parameter).
That's also my assumption.
Is BroadcastSwitchActiveComputerError extended by KeyboardError and MouseError?
No, I changed some packages to simplify samples. Let me just to publish a small project that can help us.
@mkouba https://github.com/gonciarz/zero-kvm branch: init
Hm, it looks fairly complex. What should I do to reproduce the problem?
I've tried to build the project but I get:
> Task :compileKotlin
w: /opt/source/protean/bugs/issue-29789/zero-kvm/src/main/kotlin/rg/zerokvm/adapter/spi/publisher/zmq/publisherZmqSpiAdapter.kt: (12, 5): Parameter 'computer' is never used
w: /opt/source/protean/bugs/issue-29789/zero-kvm/src/main/kotlin/rg/zerokvm/adapter/spi/publisher/zmq/publisherZmqSpiAdapter.kt: (16, 5): Parameter 'assignedDeviceSuccess' is never used
Hi, I'm sorry.
You may try to remove @Named
annotation from RestApiConfig's bean or and/or from BroadcastSwitchActiveComputerRestApiAdapter's injection or any other beans definitions and their injection points.
And then just run quarkusDev gradle's target.
I'll try to simplify the project so it contains even less code.
Sorry my mobile started clicking randomly on a screen and accidentally closed the ticket. I reopened it.
@mkouba
It contains two beans:
typealias AssignKeyboard = suspend (String) -> Either<KeyboardError, Unit>
typealias AssignMouse = suspend (String) -> Either<MouseError, Unit>
@ApplicationScoped
@Named(BeanName.ASSIGN_KEYBOARD)
fun assignLocalKeyboardBean(shellAdapter: ShellAdapter): AssignKeyboard = // code
@ApplicationScoped
@Named(BeanName.ASSIGN_MOUSE)
fun assignLocalMouseBean(shellAdapter: ShellAdapter): AssignMouse = // code
And one injection:
class KeyboardController(
@Named(BeanName.ASSIGN_KEYBOARD) private val assignKeyboard: AssignKeyboard,
) {
// code
}
Just remove @Named
to reproduce AmbiguousResolutionException
You may also have a look at Quarkus Junit's test. Field injection doesn't work with Generic type:
@Inject
@Named(BeanName.ASSIGN_KEYBOARD)
private lateinit var assignKeyboard: AssignKeyboard
And InjectMock (mockk extension) as well:
@InjectMock
@Named(BeanName.ASSIGN_KEYBOARD)
private lateinit var assignKeyboard: AssignKeyboard
Thanks @gonciarz. I was able to reproduce the problem locally:
Caused by: javax.enterprise.inject.AmbiguousResolutionException: Ambiguous dependencies for type kotlin.jvm.functions.Function2<? super java.lang.String, ? super kotlin.coroutines.Continuation<? super arrow.core.Either<? extends rg.zerokvm.KeyboardError, kotlin.Unit>>, ? extends java.lang.Object> and qualifiers [@Default]
- java member: rg.zerokvm.KeyboardController():assignKeyboard
- declared on CLASS bean [types=[rg.zerokvm.KeyboardController, java.lang.Object], qualifiers=[@Default, @Any], target=rg.zerokvm.KeyboardController]
- available beans:
- PRODUCER METHOD bean [types=[kotlin.Function<java.lang.Object>, kotlin.jvm.functions.Function2<java.lang.String, kotlin.coroutines.Continuation<? super arrow.core.Either<? extends rg.zerokvm.MouseError, kotlin.Unit>>, java.lang.Object>, java.lang.Object], qualifiers=[@Default, @Any], target=kotlin.jvm.functions.Function2<java.lang.String, kotlin.coroutines.Continuation<? super arrow.core.Either<? extends rg.zerokvm.MouseError, kotlin.Unit>>, java.lang.Object> assignLocalMouseBean(rg.zerokvm.ShellAdapter shellAdapter), declaringBean=rg.zerokvm.AppConfig]
- PRODUCER METHOD bean [types=[kotlin.Function<java.lang.Object>, kotlin.jvm.functions.Function2<java.lang.String, kotlin.coroutines.Continuation<? super arrow.core.Either<? extends rg.zerokvm.KeyboardError, kotlin.Unit>>, java.lang.Object>, java.lang.Object], qualifiers=[@Default, @Any], target=kotlin.jvm.functions.Function2<java.lang.String, kotlin.coroutines.Continuation<? super arrow.core.Either<? extends rg.zerokvm.KeyboardError, kotlin.Unit>>, java.lang.Object> assignLocalKeyboardBean(rg.zerokvm.ShellAdapter shellAdapter), declaringBean=rg.zerokvm.AppConfig]
And the required type is: kotlin.jvm.functions.Function2<? super java.lang.String, ? super kotlin.coroutines.Continuation<? super arrow.core.Either<? extends rg.zerokvm.KeyboardError, kotlin.Unit>>, ? extends java.lang.Object>
.
So first of all, those producers are illegal according to the spec, because "If a producer method return type contains a wildcard type parameter or is an array type whose component type contains a wildcard type parameter, the container automatically detects the problem and treats it as a definition error." (https://jakarta.ee/specifications/cdi/2.0/cdi-spec-2.0.html#producer_method). In fact, ArC should throw a DefinitionException
during build. Unfortunately, we don't detect this problem correctly.
Hi @mkouba,
I have seen the spec and I see your point. But CDI spec refers to class definition and a wildcard comes directly from bytecode generation that is done under the hood. It's Any?
type (Object) because Kotlin does not support union types.
But anyway I think that the problem is not so trivial.
Coroutines are implemented using Continuation functions. Compiler analyze the context in which they can be executed and do a transformation into a state machine with local variables representing a coroutine's state. Then depending on various factor the same Continuation object (related to function invocation) can be passed from one suspended function to another if they are bounded within the same state machine. The details can be found here:
My understanding is that Continuation is just a class (CPS) that is instantiated when the suspend function is called for the first time at runtime. Whenever we call the same function from different contexts we create different Continuation instances.
Update
CDI 2.0 says about producer's definition, so I guess we should focus on a class / function definition. The question is can we can create a smart analyzer that could detect that we deal with Kotlin's suspend function and we can exclude Continuation. It depends on a context that calls it and in theory you may inject producer to different contexts.
What do you think?
@geoand / @stuartwdouglas could you advice?
I do would like to use Quarkus and I created a suspend function as a bean.
At the moment ARC can create producers from them, but in order to inject them I need to use a @Named
qualifier for both producer's definition and injection.
I really really don't see why would it's useful to create a CDI bean from a suspend function.
The whole is idea with suspend
functions is to use them to make Async code easier - converting for example a Uni
based API.
CDI is not an Async API - it allows you to wire together independent software components (classes). The invocation of the suspend functions themselves is not something CDI is concerned about, that is handled by whatever framework is being used - i.e. RESTEasy Reactive, Reactive Messaging etc.
Well, the spec is pretty clear that _"A parameterized type that contains a wildcard type parameter is not a legal bean type"_. The fact that those producers work with @Named
means that there is a bug that needs to be fixed (according to type-safe resolution rules).
Sure, I am not denying that there are some specific technicalities that need to be addressed. My point is more about the general approach
@geoand function is a first class citizen for Kotlin. It has it's representation as a: kotlin.jvm.functions.Function{0,1,2..} Of course I can replace it with a class with constructor arguments, but I can also apply "a default" argument with arrow framework (by partially1 extension). This is useful for functional programming style.
The whole is idea with suspend functions is to use them to make Async code easier
exactly suspend is a built-in mechanism to suggest the injected function will run in an async, non-blocking way
I've made an exercise and compared how Spring can handle the code almost the same as in the example:
https://github.com/quarkusio/quarkus/issues/29789#issuecomment-1347419777
And I was able to not only register beans (suspend function) but also inject them without additional @Named
qualifier.
Maybe you may consider their approach?
And a mentioned function doesn't use wildcard definition as I mentioned.
typealias AssignKeyboard = suspend (String) -> Either<KeyboardError, Unit>
Wildcard is introduced to a bytecode representation. Can you explain me in two words how current ARC approach works producer registration/injection or can you point me to some doc. I guess that suspend function doesn't fit for your model in the current form, does it?
I assume @mkouba will look into the details he mentioned above.
Wildcard is introduced to a bytecode representation.
The thing is that ArC, and Quarkus in general, works with the bytecode. Our extensions do not see your kotlin source but the generated bytecode.
Can you explain me in two words how current ARC approach works producer registration/injection or can you point me to some doc.
Not really in two words. A CDI producer is a method/field of a bean that serves as a source of another bean. A simple example is described here. The way it works is that we collect all producer methods/fields of a bean class, analyze the return type, etc. and register them as producer beans. You can read more details about producers in the spec.
I guess that suspend function doesn't fit for your model in the current form, does it?
I'm not quite sure.
FYI we've just merged a fix for detection of wildcards and your app will simply fail during build now.
Thank you @mkouba
I also raised a general question on stackoverflow about CDI and suspend functions: https://stackoverflow.com/questions/74827454/should-kotlins-suspend-function-be-allowed-for-valid-cdi-producers-beans
Not really in two words. A CDI producer is a method/field of a bean that serves as a source of another bean. A simple example is described here. The way it works is that we collect all producer methods/fields of a bean class, analyze the return type, etc. and register them as producer beans. You can read more details about producers in the spec.
Thanks, I might have a look at it more deeply if I only find some time.
FYI we've just merged https://github.com/quarkusio/quarkus/pull/29883 for detection of wildcards and your app will simply fail during build now.
It's not a good info for me but thanks. I may always switch to spring native or restructure the code.
Aside: I'd love to know why a parameterized type with a wildcard type argument is not a legal bean type. The following types are legal bean types:
List<T>
, where T
is a type variableList<String>
(where String
is obviously an actual type)On the other hand, List<? extends CharSequence>
is not.
If I had a ReadonlyList
class, I'd define its type parameter as covariant in a language with declaration-site variance (such as Kotlin), so that ReadonlyList<String>
is a subtype of ReadonlyList<CharSequence>
. That isn't possible in Java, but it directly translates as a wildcard with upper bound on the use-site (that's what the Kotlin compiler generates). So I'd often have ReadonlyList<? extends CharSequence>
, and I don't really see why this can't be a legal bean type.
@mkouba @manovotn do you maybe remember the reasoning behind this? Are there perhaps types with wildcards that for good reason cannot be legal bean types, and there's no straightforward way to distinguish them from those that could be legal?
do you maybe remember the reasoning behind this? Are there perhaps types with wildcards that for good reason cannot be legal bean types, and there's no straightforward way to distinguish them from those that could be legal?
I have no idea. But it was defined in CDI 1.0: https://docs.jboss.org/cdi/spec/1.0/html/concepts.html#legalbeantypes, i.e. a long time before I joined the expert group.
Just for reference, I added that info to stack overflow question, but here are the repos with example code for all 3 frameworks:
do you maybe remember the reasoning behind this? Are there perhaps types with wildcards that for good reason cannot be legal bean types, and there's no straightforward way to distinguish them from those that could be legal?
I have no idea. But it was defined in CDI 1.0: https://docs.jboss.org/cdi/spec/1.0/html/concepts.html#legalbeantypes, i.e. a long time before I joined the expert group.
I am in the same boat.
Since you can have wildcard in injection point, I would assume there is a scenario where having a wildcard also in a bean type would result in unexpected assignment. Either that or the inability to correctly deduce set of bean types due to wildcard not being specific enough. But I cannot think of a specific example which exhibits any issues with it and version 1.0 is so old that there is pretty much no way to dig up information on why it was designed in this particular way...
FYI I also tried browsing through some TCKs, Weld tests and old JIRA issues but found no mention of this.
FTR the typesafe resolution rules (Assignability of raw and parameterized types) are consistent and do not cover the case where the bean type parameter is a wildcard. So it was not an oversight...
Good point. On the other hand, the assignability rules cover the cases when the required type is a parameterized type with a wildcard type argument, so maybe making such type a legal bean type would lead to more cases of ambiguity...? 🤷
the assignability rules cover the cases when the required type is a parameterized type with a wildcard type argument
Yes, that's a wilcard used in an injection point, i.e. perfectly legal according to https://jakarta.ee/specifications/cdi/2.0/cdi-spec-2.0.html#legal_injection_point_types.
By the way, if we follow the Java assignability rules, then a producer method like this:
List<? extends CharSequence> list() {
return List.of("foo");
}
would only be assignable to injection points like List<? extends CharSequence>
, List<?>
and List
.
BTW one other reason I can think of is plain old consistency - you couldn't declare class-based bean with wildcard (Java won't allow that) but producer method/field could do that - and also via CDI extensions. That's not a strong reason, just thinking aloud :)
By the way, if we follow the Java assignability rules, then a producer method like this:
List<? extends CharSequence> list() { return List.of("foo"); }
would only be assignable to injection points like
List<? extends CharSequence>
,List<?>
andList
.
Right. I think the point is that a bean with bean type List<? extends String>
(as well as List<String>
, but that is already allowed today) would be assignable to List<? extends CharSequence>
.
BTW one other reason I can think of is plain old consistency - you couldn't declare class-based bean with wildcard (Java won't allow that) but producer method/field could do that - and also via CDI extensions. That's not a strong reason, just thinking aloud :)
I'm not 1000% sure, as I'm writing from top of my head, but I believe you can very easily do this:
public class MyBean implements List<? extends CharSequence> {
...
}
If parameterized types with wildcard type arguments were legal bean types, and the class had a BDA, then one of the bean types could be List<? extends CharSequence>
.
I'm not 1000% sure, as I'm writing from top of my head, but I believe you can very easily do this:
public class MyBean implements List<? extends CharSequence> { ... }
You can't - a supertype may not specify a wildcard.
You're right, my bad: https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.1.5
Description
Currently the only way to avoid AmbiguousResolutionException when injecting a function using generic type is to add
@Named
qualifier. That's an overhead. A CDI framework could do that for us.Example code:
Implementation ideas
No response