diff --git a/arrow-libs/fx/arrow-fx-stm-compose/src/commonMain/kotlin/arrow/fx/stm/compose/STM.kt b/arrow-libs/fx/arrow-fx-stm-compose/src/commonMain/kotlin/arrow/fx/stm/compose/STM.kt index 86c31665c51..59eba11f932 100644 --- a/arrow-libs/fx/arrow-fx-stm-compose/src/commonMain/kotlin/arrow/fx/stm/compose/STM.kt +++ b/arrow-libs/fx/arrow-fx-stm-compose/src/commonMain/kotlin/arrow/fx/stm/compose/STM.kt @@ -12,11 +12,50 @@ public interface STM { } public fun TVar.read(): A = variable.value - public fun TVar.write(value: A) { - variable.value = value + public fun TVar.write(value: A) { variable.value = value } + public fun TVar.modify(f: (A) -> A) { variable.value = f(variable.value) } + + public operator fun TMap.get(k: K): V? = map[k] + public fun TMap.lookup(k: K): V? = this[k] + + public operator fun TMap.set(k: K, v: V) { map[k] = v } + public operator fun TMap.plusAssign(kv: Pair) { this[kv.first] = kv.second } + public fun TMap.insert(k: K, v: V) { this[k] = v } + public fun TMap.remove(k: K) { map.remove(k) } + public operator fun TMap.contains(k: K): Boolean = k in map + public fun TMap.member(k: K): Boolean = k in this + public fun TMap.update(k: K, f: (V) -> V) { + when (val v = map[k]) { + null -> {} + else -> map[k] = f(v) + } } - public fun TVar.modify(f: (A) -> A) { - variable.value = f(variable.value) + + public operator fun TSet.plusAssign(a: A) { map[a] = true } + public fun TSet.insert(a: A) { this += a } + public fun TSet.remove(a: A) { map.remove(a) } + public operator fun TSet.contains(a: A): Boolean = a in map + public fun TSet.member(a: A): Boolean = a in this + + public fun TQueue.isEmpty(): Boolean = list.isEmpty() + public fun TQueue.isNotEmpty(): Boolean = !isEmpty() + public fun TQueue.size(): Int = list.size + + public fun TQueue.peek(): A = if (list.isEmpty()) retry() else list.first() + public fun TQueue.tryPeek(): A? = list.firstOrNull() + public fun TQueue.read(): A = if (list.isEmpty()) retry() else list.removeFirst() + public fun TQueue.tryRead(): A? = if (list.isEmpty()) null else list.removeFirst() + + public operator fun TQueue.plusAssign(a: A) { list.add(a) } + public fun TQueue.write(a: A) { this += a } + public fun TQueue.writeFront(a: A) { list.add(0, a) } + + public fun TQueue.removeAll(predicate: (A) -> Boolean) { list.removeAll { !predicate(it) } } + + public fun TQueue.flush(): List { + val current = mutableListOf().also { it.addAll(list) } + list.clear() + return current } } diff --git a/arrow-libs/fx/arrow-fx-stm-compose/src/commonMain/kotlin/arrow/fx/stm/compose/Structures.kt b/arrow-libs/fx/arrow-fx-stm-compose/src/commonMain/kotlin/arrow/fx/stm/compose/Structures.kt index 7f92ac6f048..9718f8863fb 100644 --- a/arrow-libs/fx/arrow-fx-stm-compose/src/commonMain/kotlin/arrow/fx/stm/compose/Structures.kt +++ b/arrow-libs/fx/arrow-fx-stm-compose/src/commonMain/kotlin/arrow/fx/stm/compose/Structures.kt @@ -1,7 +1,11 @@ package arrow.fx.stm.compose import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.snapshots.SnapshotStateMap import kotlin.jvm.JvmInline @JvmInline @@ -12,3 +16,31 @@ public value class TVar(internal val variable: MutableState) { public fun new(value: A): TVar = TVar(mutableStateOf(value)) } } + +@JvmInline +public value class TMap(internal val map: SnapshotStateMap) { + public companion object { + public fun new(): TMap = TMap(mutableStateMapOf()) + } +} + +@JvmInline +public value class TSet(internal val map: SnapshotStateMap) { + public companion object { + public fun new(): TSet = TSet(mutableStateMapOf()) + } +} + +@JvmInline +public value class TList(internal val list: SnapshotStateList) { + public companion object { + public fun new(): TList = TList(mutableStateListOf()) + } +} + +@JvmInline +public value class TQueue(internal val list: SnapshotStateList) { + public companion object { + public fun new(): TQueue = TQueue(mutableStateListOf()) + } +} diff --git a/arrow-libs/fx/arrow-fx-stm-compose/src/commonTest/kotlin/arrow/fx/stm/compose/TMapTest.kt b/arrow-libs/fx/arrow-fx-stm-compose/src/commonTest/kotlin/arrow/fx/stm/compose/TMapTest.kt new file mode 100644 index 00000000000..881f9f5707b --- /dev/null +++ b/arrow-libs/fx/arrow-fx-stm-compose/src/commonTest/kotlin/arrow/fx/stm/compose/TMapTest.kt @@ -0,0 +1,64 @@ +package arrow.fx.stm.compose + +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.map +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class TMapTest { + + @Test fun insertValues() = runTest { + checkAll(Arb.int(), Arb.int()) { k, v -> + val map = TMap.new() + atomically { map.insert(k, v) } + atomically { map.lookup(k) } shouldBe v + } + } + + @Test fun insertMultipleValues() = runTest { + checkAll(Arb.map(Arb.int(), Arb.int())) { pairs -> + val map = TMap.new() + atomically { + for ((k, v) in pairs) map.insert(k, v) + } + atomically { + for ((k, v) in pairs) map.lookup(k) shouldBe v + } + } + } + + @Test fun insertMultipleCollidingValues() = runTest { + checkAll(Arb.map(Arb.int(), Arb.int())) { pairs -> + val map = TMap.new() // hash function that always returns 0 + atomically { + for ((k, v) in pairs) map.insert(k, v) + } + atomically { + for ((k, v) in pairs) map.lookup(k) shouldBe v + } + } + } + + @Test fun insertAndRemove() = runTest { + checkAll(Arb.int(), Arb.int()) { k, v -> + val map = TMap.new() + atomically { map.insert(k, v) } + atomically { map.lookup(k) } shouldBe v + atomically { map.remove(k) } + atomically { map.lookup(k) } shouldBe null + } + } + + @Test fun update() = runTest { + checkAll(Arb.int(), Arb.int(), Arb.int()) { k, v, g -> + val map = TMap.new() + atomically { map.insert(k, v) } + atomically { map.lookup(k) } shouldBe v + atomically { map.update(k) { v + g } } + atomically { map.lookup(k) } shouldBe v + g + } + } +} diff --git a/arrow-libs/fx/arrow-fx-stm-compose/src/commonTest/kotlin/arrow/fx/stm/compose/TQueueTest.kt b/arrow-libs/fx/arrow-fx-stm-compose/src/commonTest/kotlin/arrow/fx/stm/compose/TQueueTest.kt new file mode 100644 index 00000000000..8f28ff90b0d --- /dev/null +++ b/arrow-libs/fx/arrow-fx-stm-compose/src/commonTest/kotlin/arrow/fx/stm/compose/TQueueTest.kt @@ -0,0 +1,139 @@ +package arrow.fx.stm.compose + +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import kotlin.random.Random +import kotlin.test.Test + +class TQueueTest { + + @Test fun writingToAQueueAddsAnElement() = runTest { + val tq = TQueue.new() + atomically { tq.write(10) } + atomically { tq.flush() } shouldBe listOf(10) + } + + @Test fun readingFromAQueueShouldRetryIfTheQueueIsEmpty() = runTest { + val tq = TQueue.new() + atomically { + stm { tq.read().let { true } } orElse { false } + } shouldBe false + } + + @Test fun readingFromAQueueShouldRemoveThatValue() = runTest { + val tq = TQueue.new() + atomically { tq.write(10); tq.write(20) } + atomically { tq.read() } shouldBe 10 + atomically { tq.flush() } shouldBe listOf(20) + } + + @Test fun tryReadBehavesLikeReadIfThereAreValuesToRead() = runTest { + val tq = TQueue.new() + atomically { tq.write(10) } + atomically { tq.tryRead() } shouldBe 10 + atomically { tq.flush() } shouldBe emptyList() + } + + @Test fun tryReadReturnsNullIfTheQueueIsEmpty() = runTest { + val tq = TQueue.new() + atomically { tq.tryRead() } shouldBe null + } + + @Test fun flushEmptiesTheEntireQueueAndReturnsIt() = runTest { + val tq = TQueue.new() + atomically { tq.write(20); tq.write(30); tq.write(40) } + atomically { tq.flush() } shouldBe listOf(20, 30, 40) + atomically { tq.flush() } shouldBe emptyList() + } + + @Test fun readingFlushingShouldWorkAfterMixedReadsWrites() = runTest { + val tq = TQueue.new() + atomically { tq.write(20); tq.write(30); tq.peek(); tq.write(40) } + atomically { tq.read() } shouldBe 20 + atomically { tq.flush() } shouldBe listOf(30, 40) + + atomically { tq.write(20); tq.write(30); tq.peek(); tq.write(40) } + atomically { tq.flush() } shouldBe listOf(20, 30, 40) + atomically { tq.flush() } shouldBe emptyList() + } + + @Test fun peekShouldLeaveTheQueueUnchanged() = runTest { + val tq = TQueue.new() + atomically { tq.write(20); tq.write(30); tq.write(40) } + atomically { tq.peek() } shouldBeExactly 20 + atomically { tq.flush() } shouldBe listOf(20, 30, 40) + } + + @Test fun peekShouldRetryIfTheQueueIsEmpty() = runTest { + val tq = TQueue.new() + atomically { + stm { tq.peek().let { true } } orElse { false } + } shouldBe false + } + + @Test fun tryPeekShouldBehaveLikePeekIfThereAreElements() = runTest { + val tq = TQueue.new() + atomically { tq.write(20); tq.write(30); tq.write(40) } + atomically { tq.peek() } shouldBeExactly + atomically { tq.tryPeek()!! } + atomically { tq.flush() } shouldBe listOf(20, 30, 40) + } + + @Test fun tryPeekShouldReturnNullIfTheQueueIsEmpty() = runTest { + val tq = TQueue.new() + atomically { tq.tryPeek() } shouldBe null + } + + @Test fun isEmptyAndIsNotEmptyShouldWorkCorrectly() = runTest { + val tq = TQueue.new() + atomically { tq.isEmpty() } shouldBe true + atomically { tq.isNotEmpty() } shouldBe false + atomically { tq.write(20) } + atomically { tq.isEmpty() } shouldBe false + atomically { tq.isNotEmpty() } shouldBe true + atomically { tq.peek(); tq.write(30) } + atomically { tq.isEmpty() } shouldBe false + atomically { tq.isNotEmpty() } shouldBe true + } + + @Test fun sizeShouldReturnTheCorrectAmount() = runTest { + checkAll(Arb.int(0..50)) { i -> + val tq = TQueue.new() + atomically { + for (j in 0..i) { + // read to swap read and write lists randomly + if (Random.nextFloat() > 0.9) tq.tryPeek() + tq.write(j) + } + } + atomically { tq.size() } shouldBeExactly i + 1 + } + } + + @Test fun writeFrontShouldWorkCorrectly() = runTest { + val tq = TQueue.new() + atomically { tq.writeFront(203) } + atomically { tq.peek() } shouldBeExactly 203 + atomically { tq.writeFront(50) } + atomically { tq.peek() } shouldBeExactly 50 + atomically { tq.flush() } shouldBe listOf(50, 203) + } + + @Test fun removeAllShouldWork() = runTest { + val tq = TQueue.new() + atomically { tq.removeAll { true } } + atomically { tq.flush() } shouldBe emptyList() + + atomically { + for (i in 0..100) { + tq.write(i) + } + tq.removeAll { it.rem(2) == 0 } + tq.flush() + } shouldBe (0..100).filter { it.rem(2) == 0 } + } +}