Skip to content

Commit

Permalink
Implement Immutable Set/List/Map/Iterator/ListIterator
Browse files Browse the repository at this point in the history
  • Loading branch information
05nelsonm committed Jan 15, 2024
1 parent 9f48845 commit 75fe4d5
Show file tree
Hide file tree
Showing 7 changed files with 424 additions and 19 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,25 @@ Immutability utilities for Kotlin Multiplatform

### Immutable Collections

Wrapper classes that implement `Set`, `List`, and `Map` interfaces which inhibit modification via casting.

Does not add any new class references, as all `Immutable` implementations are private classes.

Top level accessor functions either return the `EmptySet`, `EmptyList`, `EmptyMap` objects,
or will copy & wrap the elements with the `Immutable` class implementation (if not already wrapped).

All collections/iterators returned by `Immutable` implementation functions are also `Immutable`.

```kotlin
fun main() {
// TODO
immutableSetOf("This", "That")
immutableListOf("This", "That")
immutableMapOf("This" to "That")

// toImmutable
setOf("This", "That").toImmutableSet()
listOf("This", "That").toImmutableList()
mapOf("This" to "That").toImmutableMap()
}
```

Expand Down
9 changes: 9 additions & 0 deletions library/collections/api/collections.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public final class io/matthewnelson/immutable/collections/Immutable {
public static final fun listOf (Ljava/util/Collection;)Ljava/util/List;
public static final fun listOf ([Ljava/lang/Object;)Ljava/util/List;
public static final fun mapOf (Ljava/util/Map;)Ljava/util/Map;
public static final fun mapOf ([Lkotlin/Pair;)Ljava/util/Map;
public static final fun setOf (Ljava/util/Collection;)Ljava/util/Set;
public static final fun setOf ([Ljava/lang/Object;)Ljava/util/Set;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
@file:JvmName("Immutable")

package io.matthewnelson.immutable.collections

import kotlin.jvm.JvmName

@JvmName("listOf")
public fun <T> Collection<T>.toImmutableList(): List<T> {
if (isEmpty()) return emptyList()
if (this is ImmutableList<T>) return this
return ImmutableList(toList())
}

@JvmName("listOf")
public fun <T> immutableListOf(vararg elements: T): List<T> {
if (elements.isEmpty()) return emptyList()
return ImmutableList(elements.toList())
}

@JvmName("mapOf")
public fun <K, V> Map<K, V>.toImmutableMap(): Map<K, V> {
if (isEmpty()) return emptyMap()
if (this is ImmutableMap<K, V>) return this
return ImmutableMap(toMap())
}

@JvmName("mapOf")
public fun <K, V> immutableMapOf(vararg pairs: Pair<K, V>): Map<K, V> {
if (pairs.isEmpty()) return emptyMap()
return ImmutableMap(pairs.toMap())
}

@JvmName("setOf")
public fun <T> Collection<T>.toImmutableSet(): Set<T> {
if (isEmpty()) return emptySet()
if (this is ImmutableSet<T>) return this
return ImmutableSet(toSet())
}

@JvmName("setOf")
public fun <T> immutableSetOf(vararg elements: T): Set<T> {
if (elements.isEmpty()) return emptySet()
return ImmutableSet(elements.toSet())
}

private open class ImmutableCollection<T, D: Collection<T>>(
protected val delegate: D
): Collection<T> {
final override val size: Int get() = delegate.size
final override fun isEmpty(): Boolean = delegate.isEmpty()
final override operator fun iterator(): Iterator<T> = ImmutableIterator(delegate.iterator())
final override fun containsAll(elements: Collection<T>): Boolean = delegate.containsAll(elements)
final override operator fun contains(element: T): Boolean = delegate.contains(element)

final override fun equals(other: Any?): Boolean = delegate == other
final override fun hashCode(): Int = delegate.hashCode()
final override fun toString(): String = delegate.toString()
}

private class ImmutableList<T>(
delegate: List<T>
): ImmutableCollection<T, List<T>>(delegate), List<T> {
override operator fun get(index: Int): T = delegate[index]
override fun indexOf(element: T): Int = delegate.indexOf(element)
override fun lastIndexOf(element: T): Int = delegate.lastIndexOf(element)
override fun listIterator(): ListIterator<T> = ImmutableListIterator(delegate.listIterator())
override fun listIterator(index: Int): ListIterator<T> = ImmutableListIterator(delegate.listIterator(index))
override fun subList(fromIndex: Int, toIndex: Int): List<T> = delegate.subList(fromIndex, toIndex).toImmutableList()
}

private class ImmutableSet<T>(
delegate: Set<T>
): ImmutableCollection<T, Set<T>>(delegate), Set<T>

private class ImmutableMap<K, V>(
private val delegate: Map<K, V>
): Map<K, V> {

override val entries: Set<Map.Entry<K, V>> by lazy {
val entries = delegate.entries
val set = LinkedHashSet<ImmutableMapEntry<K, V>>(entries.size, 1.0F)
entries.mapTo(set) { ImmutableMapEntry(it) }
ImmutableSet(set)
}
override val keys: Set<K> by lazy { ImmutableSet(delegate.keys) }
override val size: Int get() = delegate.size
override val values: Collection<V> by lazy { ImmutableCollection(delegate.values) }
override fun isEmpty(): Boolean = delegate.isEmpty()
override operator fun get(key: K): V? = delegate[key]
override fun containsValue(value: V): Boolean = delegate.containsValue(value)
override fun containsKey(key: K): Boolean = delegate.containsKey(key)

override fun equals(other: Any?): Boolean = delegate == other
override fun hashCode(): Int = delegate.hashCode()
override fun toString(): String = delegate.toString()
}

private class ImmutableMapEntry<K, V>(
private val delegate: Map.Entry<K, V>,
): Map.Entry<K, V> {
override val key: K get() = delegate.key
override val value: V get() = delegate.value

override fun equals(other: Any?): Boolean = delegate == other
override fun hashCode(): Int = delegate.hashCode()
override fun toString(): String = delegate.toString()
}

private open class ImmutableIterator<T, D: Iterator<T>>(
protected val delegate: D
): Iterator<T> {
final override operator fun hasNext(): Boolean = delegate.hasNext()
final override operator fun next(): T = delegate.next()

final override fun equals(other: Any?): Boolean = delegate == other
final override fun hashCode(): Int = delegate.hashCode()
final override fun toString(): String = delegate.toString()
}

private class ImmutableListIterator<T>(
delegate: ListIterator<T>
): ImmutableIterator<T, ListIterator<T>>(delegate), ListIterator<T> {
override fun hasPrevious(): Boolean = delegate.hasPrevious()
override fun nextIndex(): Int = delegate.nextIndex()
override fun previous(): T = delegate.previous()
override fun previousIndex(): Int = delegate.previousIndex()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package io.matthewnelson.immutable.collections

import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

class ImmutableListUnitTest {

private val list = listOf("Hello", "World")

@Test
fun givenList_whenToImmutableList_thenReturnsImmutableList() {
assertNotEquals("ImmutableList", list::class.simpleName)
assertEquals("ImmutableList", list.toImmutableList()::class.simpleName)
assertEquals("ImmutableList", immutableListOf("Hello", "World")::class.simpleName)
}

@Test
fun givenEmpty_whenToImmutableList_thenReturnsEmptyList() {
assertEquals("EmptyList", listOf<String>().toImmutableList()::class.simpleName)
assertEquals("EmptyList", immutableListOf<String>()::class.simpleName)
}

@Test
fun givenImmutableList_whenListIterator_thenReturnsImmutableIterators() {
assertEquals("ImmutableListIterator", list.toImmutableList().listIterator()::class.simpleName)
assertEquals("ImmutableListIterator", list.toImmutableList().listIterator(index = 1)::class.simpleName)
}

@Test
fun givenList_whenToImmutableList_thenInitialListIsCopied() {
val mutable = list.toMutableList()
val immutable = mutable.toImmutableList()
assertContentEquals(mutable, immutable)
assertEquals(mutable, immutable)

mutable.add("Something")
assertNotEquals(mutable, immutable)
}

@Test
fun givenImmutableList_whenSubList_thenReturnsImmutableList() {
assertEquals("ImmutableList", list.toImmutableList().subList(0, 1)::class.simpleName)
assertEquals("EmptyList", list.toImmutableList().subList(0, 0)::class.simpleName)
}

// ImmutableCollection
@Test
fun givenImmutableList_whenIterator_thenReturnsImmutableIterator() {
assertEquals("ImmutableIterator", list.toImmutableList().iterator()::class.simpleName)
}

// ImmutableCollection
@Test
fun givenImmutableList_whenEqualsHashCodeToString_thenIsSameAsUnderlying() {
assertEquals(list, list.toImmutableList())
assertContentEquals(list, list.toImmutableList())
assertEquals(list.hashCode(), list.toImmutableList().hashCode())
assertEquals(list.toString(), list.toImmutableList().toString())
}

// ImmutableCollection
@Test
fun givenImmutableList_whenSize_thenIsSameAsUnderlying() {
assertEquals(list.size, list.toImmutableList().size)
}

// ImmutableCollection
@Test
fun givenImmutableList_whenContainsAll_thenIsSameAsUnderlying() {
assertEquals(list.containsAll(list), list.toImmutableList().containsAll(list))
assertEquals(list.containsAll(listOf("false")), list.toImmutableList().containsAll(listOf("false")))
assertEquals(list.contains("false"), list.toImmutableList().contains("false"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package io.matthewnelson.immutable.collections

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

class ImmutableMapUnitTest {

private val map = mapOf("Hello" to "World", "H" to "W")

@Test
fun givenMap_whenToImmutableMap_thenReturnsImmutableMap() {
assertNotEquals("ImmutableMap", map::class.simpleName)
assertEquals("ImmutableMap", map.toImmutableMap()::class.simpleName)
assertEquals("ImmutableMap", immutableMapOf("Hello" to "World")::class.simpleName)
}

@Test
fun givenEmpty_whenToImmutableMap_thenReturnsEmptyMap() {
assertEquals("EmptyMap", mapOf<String, String>().toImmutableMap()::class.simpleName)
assertEquals("EmptyMap", immutableMapOf<String, String>()::class.simpleName)
}

@Test
fun givenMap_whenToImmutableMap_thenInitialMapIsCopied() {
val mutable = map.toMutableMap()
val immutable = mutable.toImmutableMap()
assertEquals(mutable, immutable)
mutable["aaa"] = "bbb"

assertNotEquals(mutable, immutable)
}

@Test
fun givenImmutableMap_whenEntries_thenAreImmutable() {
val entries = map.toImmutableMap().entries
assertEquals("ImmutableSet", entries::class.simpleName)
assertEquals("ImmutableMapEntry", entries.first()::class.simpleName)
assertEquals(map.entries, entries)
}

@Test
fun givenImmutableMap_whenKeys_thenAreImmutable() {
assertEquals("ImmutableSet", map.toImmutableMap().keys::class.simpleName)
}

@Test
fun givenImmutableMap_whenValues_thenAreImmutable() {
assertEquals("ImmutableCollection", map.toImmutableMap().values::class.simpleName)
}

@Test
fun givenImmutableMap_whenEqualsHashCodeToString_thenIsSameAsUnderlying() {
assertEquals(map, map.toImmutableMap())
assertEquals(map.hashCode(), map.toImmutableMap().hashCode())
assertEquals(map.toString(), map.toImmutableMap().toString())
}

@Test
fun givenImmutableMap_whenSize_thenIsSameAsUnderlying() {
assertEquals(map.size, map.toImmutableMap().size)
}

@Test
fun givenImmutableMap_whenContainsKey_thenIsSameAsUnderlying() {
assertEquals(map.containsKey("Hello"), map.toImmutableMap().containsKey("Hello"))
}

@Test
fun givenImmutableMap_whenContainsValue_thenIsSameAsUnderlying() {
assertEquals(map.containsKey("World"), map.toImmutableMap().containsKey("World"))
}
}
Loading

0 comments on commit 75fe4d5

Please sign in to comment.