Behavior of mutable list differs in kotlinc and REPL ki

I ran the following code in both kotlinc and ki (github)

data class Person(val name: String, val roles: List<String>)

val m = Person("bob", mutableListOf("developer"))
m.roles.addFirst("a")

In kotlinc, I got an error message:

> kotlinc -script Immutable.kts 
Immutable.kts:4:9: error: unresolved reference 'addFirst'.
m.roles.addFirst("a")
        ^

However, ki allowed me to modify the mutable list roles:

> ki
ki-shell 0.5.2/1.7.0
type :h for help
[0] data class Person(val name: String, val roles: List<String>)
[1] val m = Person("bob", mutableListOf("developer"))
[2] m.roles.addFirst("a")
[3] m.roles
res3: List<String> = [a, developer]

So, why the code returns two different outcomes depending on how it was executed?

I got confused by the use of listOf() and mutableListOf(). I understand that listOf() creates an immutable list (addFirst() is not available, so we cannot change the list), whereas mutableListOf() returns a mutable list (addFirst() is available, so we can change the list). However, that behavior does not apply when using data classes in kotlinc, is that correct? Such a difference is expected when using ki?

I am using these versions:

$ kotlinc -version
info: kotlinc-jvm 2.0.0 (JRE 22+36-2370)

$ javac -version
javac 22

$ ki --version
ki-shell 0.5.2/1.7.0

I got this answer from StackOverflow:

This isn’t about data classes as such — just about declared types. In both versions of your code, you’re creating a MutableList, but storing it in a List property. MutableList is a subtype of List, so it’s OK to store it there — that’s an upcast. From then on, the compiled code knows only that it’s a List. (After all, you could easily write some other code which stored an immutable list in that property.) However, Ki can do something that a compiler can’t: it can check the actual type at runtime.

And here, it seems to see that although the reference is of a List type, at this point in the run the object it’s actually referring to is a MutableList, and so it’s OK to treat it as mutable. — Now, whether a REPL should be doing this is another question… (I don’t know Ki; it’s possible that this is unintended behaviour!)

Is that answer correct? If so, is it OK for kotlinc and ki to treat mutable lists differently?

I think ki perhaps is using a newer version of java which has List.addFirst while kotlinc is using a different version that doesn’t

That’s interesting — but I don’t think that could fully explain it. Even if java.util.List has an addFirst() method, kotlin.List won’t because it doesn’t include mutation methods. (In fact, the current version of kotlin.MutableList doesn’t have it either; you’d need to downcast to a Java ≥21 type to see it.)

So as far as I can see, the Kotlin Language Interactive Shell (ki) is either using the Java List type instead of the Kotlin one (which would be almost certainly be wrong), or using the runtime type of the object (perhaps ArrayList) instead of the compile-time type of its reference (which would arguably be wrong if accidental, or confusing if intended).

As I* said, I don’t know the Kotlin Language Interactive Shell, so I can’t tell which.

(* I wrote the StackOverflow comments which OP quoted. Actually, I’m not sure it’s OK to quote SO comments in general, but I’m fine with it here, especially as OP included a link.)

Kotlin’s List maps to Java’s List be default. Yes mutation methods are excluded and placed in MutableList, but Kotlin exposes any new Java methods by default. I’ll try to find a reference for this.

I don’t use ki, but the only reason that comes to my mind is that maybe ki doesn’t just take the line of code and compiles it as a whole, but instead, parses it and executes part by part. For example, while we write a line, it already previews the result or it provides code completion based on the current value. And if it does this, then it may accidentally (or intentionally) use runtime types instead of compile types.

Anyway, this code is not a valid Kotlin code. addFirst should not be possible to call, even if it is there.

1 Like

That’s purely an implementation detail — affecting the runtime type, but not the code. kotlin.List has its own definition, and the code sees only that, regardless of whatever else the mapped interface might expose at runtime.