Make mutable classes alter or vary into immutable when passed as varargs

I have a mutableiterable or anything I deem mutable (let’s say I have a custom class I consider mutable due to its members changing and want to give it the same treatment as mutableiterable next).

I want to pass it as a vararg : Any using that keyword, alongside other objects (Int, Float, etc) I want to transform the variable into its immutable form first. What is the best way to do this? I want it to be on the fast side primarily, easy to read / understand secondary

Perhaps theres an extension I can use? or do I have to switch/when by class inside the vararg parameter haver?

e.g. MutableIterable._asVararg() = this.toList()

Alternatively, is there a way to enforce this immutability at compile time? (throw a compile error if I send what I deem immutable into the vararg : Any)

e.g. fun myFun(vararg foo : Any except MutableIterable)

Have you considered

MutableIterable.asIterable()

Or you could create a wrapper for any Iterable that implements Iterable. Wrap your real MutableIterable in this and pass the wrapper.

public class IterableWrapper(private val iterable: Iterable) : Iterable by iterable

That way you mask the mutability of any iterable at small cost (adding one function call from the wrapper to the wrapped).

Thinking about it, I would not be surprised if something already exists…

I considered toList. But I dont want someone else to forget to apply the transform before sending the mutable into the vararg, that’s why I need to enforce it at runtime inside the vararg receiving function, or at compile time (even better).

I suppose if the wrapper is the accepted arg so instead of vararg Any it is vararg: MyAcceptedWrapper and it can contain any primitive or immutable I define, then youd be forced to convert first. But checking the Anys at runtime is probably faster / less memory.

Frankly speaking, your case is pretty confusing to me. What is your real use case, why do you need to do this?

Normally, if we create a function and would like to ensure it won’t modify arguments passed to it, we just… don’t modify them. It doesn’t matter a function received a mutable collection if it didn’t modify it. Do you maybe mean the function passes these arguments to other places you don’t control and you would like to ensure they won’t modify anything? In that case I think it should be a responsibility of the function itself to make defensive copies and/or convert types - not a responsibility of callers of the function.

To answer your question directly on how to prevent this: it depends if you mean compile-time types or runtime types. Should this be allowed or not?

val list1 = mutableListOf(1, 2, 3)
val list2: List<Int> = list1
myFun(list2)

If it should be disallowed, then I don’t see how this could be done at compile time. Compiler has no idea list2 is actually a MutableList. It has to be done at runtime, preferably inside the myFun. If you mean it should be allowed to pass list2 and disallowed to pass list1, then I’m not sure what is the benefit of this as both variables point at exactly the same object and they both end up as just Any.

Or maybe you meant something entirely the opposite: you would like to make sure the caller won’t modify the object after passing it to the function? In that case, your idea is entirely incorrect. Kotlin doesn’t have a concept of immutable collections or immutable objects in general. You would need to use additional libraries or other wrappers like e.g.: GitHub - Kotlin/kotlinx.collections.immutable: Immutable persistent collections for Kotlin

Again, it all depends on your specific case, but I’m not sure what it is.

3 Likes

Given:

class Appropriate {}
class Inappropriate {
    fun toImmutable() : Appropriate
}

val foo: MutableList<whatever>
val baz: Inappropriate

desire1 is compile time errors:

fun bar(vararg how: ???) { /*...*/ }

bar(foo) // compile time error: MutableList foo cannot be used as a parameter of bar
bar(1, foo) // compile time error: MutableList foo cannot be used as a parameter of bar
bar(1, "", baz, foo, foo) // compile time error: Inappropriate baz cannot be used as a parameter of bar
bar(baz.toImmutable()) // yes
bar(foo.toList()) // yes
bar(1, foo.toList()) // yes
bar(1, "", foo.toList(), foo.toList(), foo.toList()) // yes

Desire2 is magic that doesn’t involve the developer and could be paired to the error checking:

fun bar(vararg how: Any) { /*...*/ }

bar(foo) // yes, bar magically does NOT receive foo but foo.toList()
bar(1, foo) // yes, bar magically does NOT receive foo but foo.toList()
bar(1, "", baz, foo, foo) // yes, bar magically does NOT receive baz but baz.toImmutable()
bar(foo.toList()) // yes
bar(baz.toImmutable()) // yes
bar(1, foo.toList()) // yes
bar(1, "", foo.toList(), foo.toList(), foo.toList()) // yes

Desire 3 is runtime errors:

fun bar(vararg how: Any) { /*...*/ }

bar(foo) // throws IllegalArgumentException: MutableList foo cannot be used as a parameter of bar, use toList
bar(1, foo) // throws IllegalArgumentException: MutableList foo cannot be used as a parameter of bar, use toList
bar(1, foo, foo, foo) // throws IllegalArgumentException: MutableList foo cannot be used as a 
parameter of bar, use toList
bar(1, "", baz, foo, foo) // throws IllegalArgumentException: Inappropriate baz cannot be used as a parameter of bar
bar(foo.toList()) // yes
bar(baz.toImmutable()) // yes
bar(1, foo.toList()) // yes
bar(1, "", foo.toList(), foo.toList(), foo.toList()) // yes

I wanted to learn if kotlin has something better than to loop the varargs and validate their type one by one inside the function, or to have the discipline and memory to call toList or whatever safety conversion without fail