- Type: Standard/kotlinx Library API proposal
- Author: Ilya Gorbunov
- Status: Submitted
- Prototype: In progress
- Discussion: In the issues of this repository
Provide interfaces to represent truly immutable collections and maps.
Provide implementation of those interfaces.
There are two families of collection interfaces in the Standard Library:
- read-only interfaces, which only allow to read elements from a collection;
- mutable interfaces, which additionally allow to modify elements in a collection.
Kotlin mostly doesn't provide its own collection implementations.
Its collection interfaces are mapped to the standard JDK collection interfaces
and implemented by the JDK collection implementations, such as ArrayList
, HashSet
, HashMap
and other
classes from java.util
package.
It is proposed to provide immutable collection interfaces which extend read-only collection interfaces and specify by the contract the real immutability of their implementors.
Immutable collections could have same modification operations as their mutable counterparts, such as add(element)
,
remove(element)
, put(key, value)
etc., but unlike mutating operations they do not mutate the receiver, but instead
return new immutable collection instance with the modification applied.
In case if no modification is made during the operation, for example, adding an existing element to a set or clearing already empty collection, the operation should return the same instance of the immutable collection.
Often there is a need to perform several subsequent modification operations on an immutable collection. It can be done by
-
chaining operations together:
collection = collection.add(element).add(anotherElement)
-
applying operations one by one:
collection += element collection += anotherElement
-
using collection builders.
An immutable collection builder is a wrapper around an immutable collection, which exposes its modification operations
within the mutable collection interface. For example, ImmutableSet.Builder<T>
extends MutableSet<T>
.
An immutable collection could be transformed to a builder with builder()
method, and a builder could be
transformed back to the immutable collection with its build()
method.
val builder = immutableList.builder()
builder.removeAll { it.value > treshold }
if (builder.isEmpty()) {
builder.add(defaultValue)
}
return builder.build()
When mutating operation is invoked on a builder, it doesn't mutate the original immutable collection it was built from, but instead it gradually builds new immutable collection with the requested modifications applied.
In case if each operation invoked on a builder causes no modifications,
the collection returned by build()
method is the same collection the builder was obtained from.
Builders have two advantages:
- Allow to utilize existing API taking mutable collections.
- Builders could keep their internal data structures mutable and finalize them only when
build()
method is called.
It's convenient for immutable collections to have the same variance as the read-only collections they extend.
However their modification operations could have covariant type appear in parameter (contravariant) position,
therefore that parameter type must be annotated with @UnsafeVariance
annotation, and special care must be given when
implementing such methods.
- Extends read-only Collection.
- Provides builder as a mutable collection.
- Extends read-only List and immutable Collection.
- Provides builder as a mutable list.
- Has the single implementation, returned by
immutableListOf
function.
- Extends read-only Set and immutable Collection.
- Provides builder as a mutable set.
- Has two implementations (
immutableSetOf
,immutableHashSetOf
) differing by whether the insertion order of elements is preserved.
- Extends read-only Map.
- Provides builder as a mutable map.
- Has two implementations (
immutableMapOf
,immutableHashMapOf
) differing by whether the insertion order of keys is preserved.
Converts read-only or mutable collection to immutable one. If the receiver is already immutable and has the required type, returns itself.
fun Iterable<T>.toImmutableList(): ImmutableList<T>
fun Iterable<T>.toImmutableSet(): ImmutableSet<T>
plus
and minus
operators on immutable collections can exploit their immutability
and delegate the implementation to the collections themselves.
A quite common pattern with builders arises: get a builder, apply some mutating operations on it, transform it back to an immutable collection:
collection.builder().apply { some_actions_on(this) }.build()
This pattern could be extracted to an extension function.
ImmutableList.mutate(action: (MutableList) -> Unit) =
builder().apply { action(this) }.build()
-
Avoiding defensive copying of collection passed as a parameter when you need an unchangeable snapshot and you're unsure whether the collection could be changed later.
class Query(val parameters: ImmutableList<Parameter>) // or class Query(parameters: List<Parameter>) { // creates an immutable list or does nothing if it's already immutable val parameters = parameters.toImmutable() }
-
Sharing a single immutable collection between multiple threads. Since the collection cannot be changed neither by producer, nor by any consumer, you'll never get a
ConcurrentModificationException
during iteration, or any other symptoms of corrupted state. -
Having multiple snapshots of a collection sharing their element storage.
-
Create a mutable collection, but expose it only as its read-only interface. This pattern is mostly employed in the standard library collection operations.
- Pro: mutable collection implementations are generally more memory efficient, require less allocations during and after their populating.
- Con: collection can be cast back to mutable (e.g. involving platform types), and then unexpectedly mutated.
- Con: different snapshots of a collection do not share any element storage.
- Con: defensive copying of the resulting collection is still required later in a chain of operations.
What are the dependencies of the proposed API:
- interfaces depend only on a subset of Kotlin Standard Library available on all supported platforms.
- current collection implementations are based on PCollections library, which is being shaded into the resulting artifact. This results in dependency on JVM 1.6 and upper.
- Package:
kotlin.collections.immutable
- At first the API is to be provided in a separate module
kotlin-collections-immutable
, but later it may be merged intokotlin-stdlib
.
Reference implementation is given in this repository, see the source.
- There is no implementation of immutable ordered map.
- Immutable collections currently aren't
Serializable
.
- Should we distinguish immutable and persistent collection interfaces and implementations?
- Should we provide sorted maps and sets?
- Should we expose immutable collection implementations to the public API? Current experience shows that it's not mandatory.
- Immutable map provides
keys
,entries
sets andvalues
collection. Should these collections be also immutable, or just read-only is enough? Note that their implementations are not persistent and most of their modification operations require to make a copy of the collection. Map - key
operation: we do not support such operation for read-only maps, and for mutable maps we can doMutableMap.keys -= key
. What would be the analogous operation forImmutableMap
?- Resolution: these operators are implemented in the standard library, so it's fine to implement the same for immutable maps.
mutate
extension: should the action takeMutableList
as a receiver or as a parameter (this
orit
inside lambda)?immutableMapOf<K,V>().builder()
requires explicit specification of K and V type arguments (until the common type inference system is implemented) and is quite lengthy. Should we provide some shortcut to infer types from expected map type?
- Provide other immutable collection interfaces such as
ImmutableStack
andImmutableQueue
and their implementations. - Improve standard library collection operations to exploit benefits of immutability of the source collections.