-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make all immutable interfaces sealed
to prevent implementations that allow mutations
#147
Comments
I don't think this is necessary. Wrongfully implementing an interface will always cause problems. |
If the implementation of the interface can be mutable, then it is not "immutable" it is just a readonly interface similar to |
Correct. But immutability is a contract. Just like Java's mutability is a contract broken by Guava's immutable collections. Making Consider this: |
The In the future, we might decide to limit the implementation of these interfaces or expose the implementation classes. So that users can be confident that the implementation is indeed immutable. |
I find that As an example, if I bring in 2 third party SDKs (sdk1 and sdk2) I want to be sure that val immutableList: ImmutableList = sdk1.getData()
val processedData: List = sdk2.process(immutableList)
// immutableList is unchanged |
To add to the example above, if public class Sdk2 {
static void process(List<String> list) {
list.add("foo");
}
} Also, I imagine there are potential performance gains that could be had by knowing all possible implementations of |
To further my point, if we make What you're suggesting applies to any interface: if we couple the implementation with the API (by making the interface sealed), we can get better performance and reliability. But SOLID tells us not to. The interface is open for a reason - to allow alternative implementations - for example, to wrap native list implementations - why would I make them falsely implement mutable list and then wrap them with an adapter instead of just implementing immutable list? |
I don't think ImmutableList should be a sealed interface. I think it should be a final class, not an interface at all, which is what Google Guava does. Guava does it that way for pretty much exactly the reasons described in this bug report. (Specifically, that allowing additional implementations of ImmutableList and other immutable types can create serious security holes.) This is not the first time this issue has been brought up. See for instance my comments here and here on a bug which was filed in 2017. Regarding @Laxystem's argument that this isn't SOLID: That's true, but irrelevant in this case. Immutable types are often necessary for security, and as is often true in security, maximally flexible code is often unfixably insecure code. Sometimes making sure that nobody CAN violate a contract (even if they want to!) is very important. There's a reason that java.util.String doesn't allow subclasses of it to be created. If someone wants a "kind of, mostly, sort of immutable" class that is flexible to their own needs, they can subclass the List interface and do what they like. The entire point of an ImmutableList interface, on the other hand, is that its contents are NOT, EVER, UNDER ANY CIRCUMSTANCES supposed to change, unlike other subclasses of the List interface which does not make that guarantee. The fact that an ImmutableList is truly immutable enables special behavior such as being able to "copy" by just passing references around without needing to make defensive copies of the referenced data: that would not be possible if some "kind of, mostly, sort of immutable" subclass was being used. Therefore allowing subclasses of ImmutableList to be created violates this immutability guarantee and defeats the purpose and trustworthyness of ImmutableList itself. |
@Sporking about security: in all Kotlin targets, the moment malicious code runs, you're fucked. Unless JetBrains is planning on improving security across the board in ways completely unprecedented for such a high-level language (besides Rust), I just don't see a reason for this. Kotlin relies on sandboxing for security. Yes, for languages like Rust and C that are used to write the sandbox and the OS themselves you have a point - but not for a language like Kotlin that is made to be sandboxed - Android is completely sandboxed, so are Wasm and Js; The JVM as a whole is a walking security nightmare; and Kotlin/Native exists in name only. Immutable lists serve a different point in Kotlin: thread-safety and iteration-safety. |
For example, when writing a game engine, I'm keeping vectors immutable and open; Yes, one can abuse that to render completely different things on my game's window - but that same malware can just create its own window, or encrypt your entire filesystem, because the JVM is insecure af anyway. And this is without mentioning mixins that can just inject into your immutable list implementation at runtime anyway. |
Could have a sub-interface that is sealed. Then most people can use But if you really want a restricted set of implementations, you can use interface ImmutableList
sealed interface SealedImmutableList: ImmutableList
class PersistentList: SealedImmutableList { ... }
class SmallList: SealedImmutableList { ... } |
If I am going to continue using For example (pseudocode): fun buildTree(treeReader: TreeReader): Tree? {
return when (treeReader.nextNodeType()) {
LEAF -> Tree.Leaf(treeReader.getCurrentValue())
BRANCH -> {
treeReader.stepDown()
val children = mutableListOf<Tree>()
while (true) {
val child = buildTree(treeReader)
child ?: break
children.add(child)
}
treeReader.stepUp()
// Defensive copy here would be prohibitively expensive.
Tree.Branch(ImmutableListAdapter(children))
}
null -> null
}
} I just got a significant (~75%) reduction of the memory allocation rate in the hot path of a project I maintain by getting rid of redundant defensive copying.
I think that @yogurtearl's suggestion is probably the best choice for balancing use cases like mine with the more security-focused use cases. |
I agree with that. As for implementation, I would: sealed interface ConstantCollection<T> : PersistentCollection<T>
fun <T> persistentListOf(vararg contents): ConstantList<T> // change return type
fun <T> List<T>.toConstantList(): ConstantList<T> = if (this is ConstantList) this else persistentListOf() + this // add new converters Because Extend |
class NotImmutable {
val a: MutableList<Int> get() = ArrayList()
} |
The way I see it, the source of the dispute is that we essentially have two libraries in one: immutable collections and persistent collections.
So basically, if we seal ImmutableList but keep PersistentList open, that should make everyone happy ;-) |
Regarding my use case for open immutable collections, it could also be solved if the library provided special builders for the immutable collections that avoided making defensive copies. The builder could guarantee that a mutable collection is wrapped in an immutable collection and that the mutable collection will not be leaked anywhere else. Something like this strawman example might work: class ImmutableListBuilder<T> {
private var size: Int = 0
private val elements: Array<T?>? = null;
/** Allocate new array if elements is null or if more capacity is needed */
private fun ensureCapacity(n: Int): Array<T?> = TODO()
fun add(element: T): ImmutableListBuilder<T> {
val array = ensureCapacity(size + 1)
array[size++] = element
return this
}
/** Builds an ImmutableList and clears this builder */
fun buildAndReset(): ImmutableList<T> {
// Wrap the array without making a defensive copy
val immutableList = ArrayBackedImmutableList(elements)
// Ensure that only the ArrayBackedImmutableList has a reference to the backing array
elements = null
return immutableList
}
} |
I was referring to fieldful properties. |
Right now you can roll your own mutable version of
ImmutableList
with something like this:Even well-meaning implementations of the
ImmutableList
interface might accidentally enable mutability.To make
ImmutableList
(andImmutableCollection
, etc) even more immutable, make all of the the interfaces in this librarysealed
interfaces.The text was updated successfully, but these errors were encountered: