Skip to content

Commit

Permalink
Adds a andThen extension method to Validated to chain Validated (#2539)
Browse files Browse the repository at this point in the history
  • Loading branch information
magott committed Sep 28, 2021
1 parent fac6b36 commit e30e298
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 1 deletion.
1 change: 1 addition & 0 deletions arrow-libs/core/arrow-core/api/arrow-core.api
Expand Up @@ -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;
Expand Down
Expand Up @@ -364,7 +364,71 @@ public typealias Invalid<E> = Validated.Invalid<E>
* ## 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<A> {
* abstract fun read(s: String): A?
*
* companion object {
*
* val stringRead: Read<String> =
* object : Read<String>() {
* override fun read(s: String): String? = s
* }
*
* val intRead: Read<Int> =
* object : Read<Int>() {
* override fun read(s: String): Int? =
* if (s.matches(Regex("-?[0-9]+"))) s.toInt() else null
* }
* }
* }
*
* data class Config(val map: Map<String, String>) {
* suspend fun <A> parse(read: Read<A>, key: String) = either<ConfigError, A> {
* 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
Expand Down Expand Up @@ -1152,6 +1216,22 @@ public inline fun <E, A> Validated<E, A>.findValid(SE: Semigroup<E>, 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 <E, A, B> Validated<E, A>.andThen(f: (A) -> Validated<E, B>): Validated<E, B> =
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,
Expand Down
Expand Up @@ -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)
}
}
}
}

0 comments on commit e30e298

Please sign in to comment.