Open yogurtearl opened 1 year ago
I don't think this is necessary. Wrongfully implementing an interface will always cause problems. Just because someone could implement the List interface incorrectly you would not make it sealed. Sealed interfaces should be conceptually reserved to represent closed implementation hierarchies.
If the implementation of the interface can be mutable, then it is not "immutable" it is just a readonly interface similar to List
If the implementation of the interface can be mutable, then it is not "immutable" it is just a readonly interface similar to
List
Correct. But immutability is a contract. Just like Java's mutability is a contract broken by Guava's immutable collections. Making ImmutableList
sealed will prevent people from implementing immutable collections on their own.
Consider this:
The Immutable
and Persistent
interfaces were introduced to allow users implement alternative versions that better suite their use cases. For example, in projects where collections are frequently accessed but very rarely updated, a regular copy-on-write implementation of these interfaces might be more appropriate. However, we recognize that only a small number of users might find this flexibility useful. Therefore, it is very helpful to hear from you whether you utilize this flexibility or it instead poses challenges for you.
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 kotlin.collections.List
provides the needed flexibility for alternative list implementations.
But when using ImmutableList
I am looking for the strongest possible multiplatform immutability guarantees from a security and thread safety point of view. (I know reflection, serialization, agents, etc can be used to subvert some guarantees)
As an example, if I bring in 2 third party SDKs (sdk1 and sdk2) I want to be sure that immutableList
won't change after I pass it into sdk2:
val immutableList: ImmutableList = sdk1.getData()
val processedData: List = sdk2.process(immutableList)
// immutableList is unchanged
To add to the example above, if sdk1.getData()
returned a custom mutable implementation of ImmutableList
and Sdk2 was written in Java, then Java might happily modify the list when I call sdk2.process(immutableList)
(should throw an exception).
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 ImmutableList
statically at compile time? i.e. faster forEach()
, map()
etc.
To further my point, if we make ImmutableList
sealed
, shouldn't we do the same for mutable ones, too? Afterall, the same arguments apply - the contract can be broken, it could have some performance benefits...
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.
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 makeImmutableList
(andImmutableCollection
, etc) even more immutable, make all of the the interfaces in this librarysealed
interfaces.