Skip to content
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

Open
yogurtearl opened this issue Jun 9, 2023 · 7 comments

Comments

@yogurtearl
Copy link

yogurtearl commented Jun 9, 2023

Right now you can roll your own mutable version of ImmutableList with something like this:

class SneakyList<E>(
    val mutableList: MutableList<E> = mutableListOf()
): ImmutableList<E> {
    override val size: Int
        get() = mutableList.size

    override fun get(index: Int): E = mutableList[index]
.
.
.
}

Even well-meaning implementations of the ImmutableList interface might accidentally enable mutability.
To make ImmutableList (and ImmutableCollection, etc) even more immutable, make all of the the interfaces in this library sealed interfaces.

@wzsaz
Copy link

wzsaz commented Sep 20, 2023

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.

@yogurtearl
Copy link
Author

If the implementation of the interface can be mutable, then it is not "immutable" it is just a readonly interface similar to List

@Laxystem
Copy link

Laxystem commented Apr 3, 2024

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:

https://github.com/Kotlin/kotlinx.collections.immutable/blob/1d18389e32e8afdbf5caa89808fb5af308fb5c8d/core/commonMain/src/adapters/ReadOnlyCollectionAdapters.kt#L11C1-L20C2

@qurbonzoda
Copy link
Contributor

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.

@yogurtearl
Copy link
Author

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

@yogurtearl
Copy link
Author

yogurtearl commented May 24, 2024

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.

@Laxystem
Copy link

Laxystem commented May 28, 2024

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?

This was referenced Jun 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants