Hey there, not really a "getting started" question...
# getting-started
s
Hey there, not really a "getting started" question, but it's about the language itself so I think it fits. It's about type inference. I have the following scenario: a
Base<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:
Copy code
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.0
y
Indeed 1.9 throws an error for implicitT2
IntelliJ says that for implicitT2, the type of the return value of create is
Base<Nothing> & T2
s
Simplified repro for the issue: https://pl.kotl.in/gvtww_0EN I think it's best to raise this on youtrack. I could do that later today unless some of you guys can be faster 😄
y
This fairs a little better, I had to swap Nothing for Unit, which makes better sense anyway:
Copy code
interface 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.
s
To me
Nothing
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:
Copy code
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 context
s
Thanks for the effort! @Youssef Shoaib [MOD] unfortunately this allows the opposite:
val 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 :)
y
I'm thinking of it as you're basically declaring a function
(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 sense
s
(Unit) -> Base<Unit>
is equivalent to `() -> Base<Unit>`which just means that it creates an instance with no parameters.
Is that the case? Because the compiler makes a distinction:
Copy code
val t: (Unit) -> Unit = {}
fun k() {
    t() // error
    t(Unit) // ok
}
y
Sorry I mean equivalent "morally", or more technically through an isomorphism:
Copy code
fun <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)