diff --git a/arrow-libs/core/arrow-core/api/arrow-core.api b/arrow-libs/core/arrow-core/api/arrow-core.api index 37978c410a7..830b32ac0ad 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.api @@ -2331,6 +2331,7 @@ public final class arrow/core/Validated$Valid$Companion { } public final class arrow/core/ValidatedKt { + public static final fun andThen (Larrow/core/Validated;Lkotlin/jvm/functions/Function1;)Larrow/core/Validated; public static final fun attempt (Larrow/core/Validated;)Larrow/core/Validated; public static final fun bisequence (Larrow/core/Validated;)Ljava/util/List; public static final fun bisequenceEither (Larrow/core/Validated;)Larrow/core/Either; diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Validated.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Validated.kt index 1b87b3c7d6a..fff1d002aaa 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Validated.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Validated.kt @@ -364,7 +364,71 @@ public typealias Invalid = Validated.Invalid * ## Sequential Validation * * If you do want error accumulation, but occasionally run into places where sequential validation is needed, - * then Validated provides a `withEither` method to allow you to temporarily turn a Validated + * then Validated provides a couple of methods that can be used. + * + * ### `andThen` + * + * The `andThen` method is similar to `flatMap`. In case of a valid instance it will pass the valid value into + * the supplied function that in turn returns a `Validated` instance + * + * ```kotlin:ank:playground + * import arrow.core.Validated + * import arrow.core.computations.either + * import arrow.core.valid + * import arrow.core.invalid + * + * abstract class Read { + * abstract fun read(s: String): A? + * + * companion object { + * + * val stringRead: Read = + * object : Read() { + * override fun read(s: String): String? = s + * } + * + * val intRead: Read = + * object : Read() { + * override fun read(s: String): Int? = + * if (s.matches(Regex("-?[0-9]+"))) s.toInt() else null + * } + * } + * } + * + * data class Config(val map: Map) { + * suspend fun parse(read: Read, key: String) = either { + * val value = Validated.fromNullable(map[key]) { + * ConfigError.MissingConfig(key) + * }.bind() + * val readVal = Validated.fromNullable(read.read(value)) { + * ConfigError.ParseConfig(key) + * }.bind() + * readVal + * }.toValidatedNel() + * } + * + * sealed class ConfigError { + * data class MissingConfig(val field: String) : ConfigError() + * data class ParseConfig(val field: String) : ConfigError() + * } + * + * //sampleStart + * val config = Config(mapOf("house_number" to "-42")) + * + * suspend fun main() { + * val houseNumber = config.parse(Read.intRead, "house_number").andThen { number -> + * if (number >= 0) Valid(0) + * else Invalid(ConfigError.ParseConfig("house_number")) + * } + * //sampleEnd + * println(houseNumber) + * } + * + * ``` + * + * ### `withEither` + * + * The `withEither` method to allow you to temporarily turn a Validated * instance into an Either instance and apply it to a function. * * ```kotlin:ank:playground @@ -1152,6 +1216,22 @@ public inline fun Validated.findValid(SE: Semigroup, that: () -> { Valid(it) } ) +/** + * Apply a function to a Valid value, returning a new Validation that may be valid or invalid + * + * Example: + * ``` + * Valid(5).andThen { Valid(10) } // Result: Valid(10) + * Valid(5).andThen { Invalid(10) } // Result: Invalid(10) + * Invalid(5).andThen { Valid(10) } // Result: Invalid(5) + * ``` + */ +public inline fun Validated.andThen(f: (A) -> Validated): Validated = + when (this) { + is Validated.Valid -> f(value) + is Validated.Invalid -> this + } + /** * Return this if it is Valid, or else fall back to the given default. * The functionality is similar to that of [findValid] except for failure accumulation, diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/ValidatedTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/ValidatedTest.kt index 512985f3e0d..adaeaada31a 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/ValidatedTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/ValidatedTest.kt @@ -498,5 +498,23 @@ class ValidatedTest : UnitSpec() { invalid.bitraverseEither({ it.left() }, { it.right() }) } } + + "andThen should return Valid(result) if f return Valid" { + checkAll(Arb.int(), Arb.int()) { x, y -> + Valid(x).andThen { Valid(it + y) } shouldBe Valid(x + y) + } + } + + "andThen should only run f on valid instances " { + checkAll(Arb.int(), Arb.int()) { x, y -> + Invalid(x).andThen { Valid(y) } shouldBe Invalid(x) + } + } + + "andThen should return Invalid(result) if f return Invalid " { + checkAll(Arb.int(), Arb.int()) { x, y -> + Valid(x).andThen { Invalid(it + y) } shouldBe Invalid(x + y) + } + } } }