s3rius
10/31/2024, 11:38 AMBase<C>
interface that lots of classes implement. Implementers can either use Base<Nothing>
(see T1
in example code) or Base<SomeRandomType>
(see T2
).
I can call create()
to construct an instance of that type (via DI).
It's important to me that create()
needs to be called with the correct parameter: to build an instance of T1
, nothing is required so val t1 = create<T1>()
is enough. For T2
, an int
is required so val t2 = create<T2, Int>(123)
is required.
This almost works. But when Kotlin inferences the type implicitly (val t2: T2 = create()
) then, for some reason, it allows the wrong overload of the function.
Here's the concrete example:
interface Base<C>
class T1 : Base<Nothing>
class T2 : Base<Int>
inline fun <reified T : Base<Nothing>> create(): T = TODO()
inline fun <reified T : Base<C>, C : Any> create(value: C): T = TODO()
fun test() {
val implicitT1: T1 = create() // (1) no error. Fine since T1 needs to value.
val implicitT2: T2 = create() // (2) no error. This is bad. It should require the variant of create() that takes value
val explicitT1 = create<T1>() // (3) no error. Fine.
val explicitT2 = create<T2>() // (4) "error: Type argument is not within its bounds"". This is correct.
val explicitT2Working = create<T2, _>(1234) // (5) no error. Fine.
}
Is there a way to change fun create()
in a way that (2) does not compile?
P.S. this might be dependent on the Kotlin version. I have the suspicion that this was a behavioral change between 1.x and 2.0Youssef Shoaib [MOD]
10/31/2024, 1:32 PMYoussef Shoaib [MOD]
10/31/2024, 1:34 PMBase<Nothing> & T2
Szymon Jeziorski
10/31/2024, 1:35 PMYoussef Shoaib [MOD]
10/31/2024, 1:46 PMinterface Base<C>
class T1 : Base<Unit>
class T2 : Base<Int>
inline fun <reified T : Base<C>, C : Unit> create(): T = TODO()
inline fun <reified T : Base<C>, C : Any> create(value: C): T = TODO()
fun main() {
val implicitT1: T1 = create() // (1) no error. Fine since T1 needs to value.
val implicitT2: T2 = create() // (2) error
val explicitT1 = create<T1, _>() // (3) no error. Fine.
val explicitT2 = create<T2, _>() // (4) "error: Type argument is not within its bounds"". This is correct.
val explicitT2Working = create<T2, _>(1234) // (5) no error. Fine.
}
The annoying thing is the extra type parameter.Szymon Jeziorski
10/31/2024, 1:55 PMNothing
might make more sense in a way, that it indicates that no value is possible in the context. Having class T1 : Base<Unit>
, nothing stops you from calling create(value)
to obtain T1
instance:
val implicitT1: T1 = create(Unit)
with Base<Nothing>
it wouldn't be possible.
But to be fair, don't want to judge on it without bigger contexts3rius
10/31/2024, 1:57 PMval t1: T1 = create(123) // no error although T1 is Base<Unit>
@Szymon Jeziorski Would be great if you could submit it- I don't have an account I believe. And I'm not sure I actually grasp the issue entirely :)Youssef Shoaib [MOD]
10/31/2024, 1:59 PM(T) -> Base<T>
. (Unit) -> Base<Unit>
is equivalent to () -> Base<Unit>
which just means that it creates an instance with no parameters. (Nothing) -> Base<Nothing>
however means that this type is impossible to create because you can never conjure up a value of Nothing
Then the create()
function is thus seen as sugar for create(Unit)
which again makes senses3rius
10/31/2024, 2:12 PMIs that the case? Because the compiler makes a distinction:is equivalent to `() -> Base<Unit>`which just means that it creates an instance with no parameters.(Unit) -> Base<Unit>
val t: (Unit) -> Unit = {}
fun k() {
t() // error
t(Unit) // ok
}
Youssef Shoaib [MOD]
10/31/2024, 2:15 PMfun <R> forward(f: () -> R): (Unit) -> R = { f() }
fun <R> backward(f: (Unit) -> R): () -> R = { f(Unit) }
I.e. they're the same information, just with different types, and in fact their information is that they just contain an R (or maybe they throw an exception or loop forever or so some side effect, but you get the point)