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 }
+ }
+}