From a03a43e6ca02cf55e5651cb0ecfbc03392d0997a Mon Sep 17 00:00:00 2001 From: Uberto Barbini Date: Tue, 10 Oct 2023 07:07:17 +0900 Subject: [PATCH] Traverse family of functions for R4K (#46) * Traverse family of functions for R4K Signed-off-by: Uberto Barbini * Removed println and fixed formatting Signed-off-by: Uberto Barbini * renamed tests following conventions Signed-off-by: Uberto Barbini * renamed traverse to mapAllValues following conventions Signed-off-by: Uberto Barbini * Made imperative code more obviously imperative --------- Signed-off-by: Uberto Barbini Co-authored-by: Nat Pryce --- .../dev/forkhandles/result4k/iterables.kt | 29 +++++ .../forkhandles/result4k/iterables_tests.kt | 108 ++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/result4k/core/src/main/kotlin/dev/forkhandles/result4k/iterables.kt b/result4k/core/src/main/kotlin/dev/forkhandles/result4k/iterables.kt index 6caba05..bb59b26 100644 --- a/result4k/core/src/main/kotlin/dev/forkhandles/result4k/iterables.kt +++ b/result4k/core/src/main/kotlin/dev/forkhandles/result4k/iterables.kt @@ -18,3 +18,32 @@ fun Iterable>.partition(): Pair, List> { } return Pair(oks, errs) } + +// Traverse family of functions + +fun Iterable.foldResult( + initial: Result, + operation: (acc: Tʹ, T) -> Result +): Result = + fold(initial) { acc, el -> acc.flatMap { accVal -> operation(accVal, el) } } + +fun Sequence.foldResult( + initial: Result, + operation: (acc: Tʹ, T) -> Result +): Result = + fold(initial) { acc, el -> acc.flatMap { accVal -> operation(accVal, el) } } + +fun Iterable.mapAllValues(f: (T) -> Result): Result, E> { + return mutableListOf() + .also { results -> forEach { e -> results.add(f(e).onFailure { return it }) } } + .let(::Success) +} + +fun Sequence.mapAllValues(f: (T) -> Result): Result, E> { + return mutableListOf() + .also { results -> forEach { e -> results.add(f(e).onFailure { return it }) } } + .let(::Success) +} + +fun Sequence>.allValues(): Result, E> = + mapAllValues { it } diff --git a/result4k/core/src/test/kotlin/dev/forkhandles/result4k/iterables_tests.kt b/result4k/core/src/test/kotlin/dev/forkhandles/result4k/iterables_tests.kt index 92f9738..884025b 100644 --- a/result4k/core/src/test/kotlin/dev/forkhandles/result4k/iterables_tests.kt +++ b/result4k/core/src/test/kotlin/dev/forkhandles/result4k/iterables_tests.kt @@ -1,6 +1,7 @@ package dev.forkhandles.result4k import org.junit.jupiter.api.Test +import kotlin.random.Random import kotlin.test.assertEquals class AllValuesTests { @@ -35,3 +36,110 @@ class PartitionTests { listOf(Failure("bad"), Failure("also bad")).partition()) } } + +private fun randomPositiveInt() = Random.nextInt(1, 100) + +private fun generateRandomList(): List = + listOf( + randomPositiveInt(), + randomPositiveInt(), + randomPositiveInt(), + randomPositiveInt(), + randomPositiveInt() + ) +class TraverseIterableTests { + @Test + fun `returns the first failure or the folded iterable as success`() { + val list = generateRandomList() + assertEquals(Success(list.sum()), + list.foldResult(Success(0)) { acc: Int, i: Int -> Success(acc + i) }) + } + + @Test + fun `returns the first failure or the mapping of iterable as success`() { + val list = generateRandomList() + assertEquals(Success(list.map { it * 2 }), + list.mapAllValues { i -> Success(i * 2) }) + } + + @Test + fun `returns the first failure or the iterable as success`() { + val list = generateRandomList().map { Success(it) } + assertEquals(Success(list.map { it.value }), + list.allValues()) + } + + @Test + fun `failure is returned if the folding operation fails`() { + val list = generateRandomList() + fun failingOperation(a: Int, b: Int) = Failure("Test error") + + assertEquals(Failure("Test error"), + list.foldResult(Success(100), ::failingOperation)) + } + + @Test + fun `failure is returned if the mapping operation fails`() { + val list = generateRandomList() + val failingFunction: (Int) -> Result = { _ -> Failure("Test error") } + assertEquals(Failure("Test error"), + list.mapAllValues(failingFunction)) + } + + @Test + fun `failure is returned if the iterable contained a failure`() { + val list = listOf(Success(randomPositiveInt()), Failure("Test error")) + assertEquals(Failure("Test error"), + list.allValues()) + } +} + +class TraverseSequenceTests { + + @Test + fun `returns the first failure or the folded sequence as success`() { + val list = generateRandomList() + val sequence = list.asSequence() + assertEquals(Success(list.sum()), + sequence.foldResult(Success(0)) { acc, i -> Success(acc + i) }) + } + + @Test + fun `returns the first failure or the mapping of sequence as success`() { + val list = generateRandomList() + val sequence = list.asSequence() + assertEquals(Success(list.map { it * 2 }), + sequence.mapAllValues { i -> Success(i * 2) }) + } + + @Test + fun `returns the first failure or the mapped sequence as success`() { + val list = generateRandomList() + val sequence = list.map { Success(it) }.asSequence() + assertEquals(Success(list), + sequence.allValues()) + } + + @Test + fun `failure is returned if the folding operation fails`() { + val sequence = generateSequence { randomPositiveInt() }.take(5) + val failingOperation: (Int, Int) -> Result = { _, _ -> Failure("Test error") } + assertEquals(Failure("Test error"), + sequence.foldResult(Success(0), failingOperation)) + } + + @Test + fun `failure is returned if the mapping operation fails`() { + val sequence = generateSequence { randomPositiveInt() }.take(5) + val failingFunction: (Int) -> Result = { _ -> Failure("Test error") } + assertEquals(Failure("Test error"), + sequence.mapAllValues(failingFunction)) + } + + @Test + fun `failure is returned if the sequence contained a failure`() { + val sequence = generateSequence { Success(randomPositiveInt()) }.take(5) + sequenceOf(Failure("Test error")) + assertEquals(Failure("Test error"), + sequence.allValues()) + } +}