Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a andThen extension method to Validated to chain Validated #2539

Merged
merged 1 commit into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions arrow-libs/core/arrow-core/api/arrow-core.api
Original file line number Diff line number Diff line change
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;
magott marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@magott should it rather be Valid(number) here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha, surprised there weren't more errors in code written in doc. Do you want a new PR @nomisRev ? Or will you just fix it in main?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Good job 👏 Looking forward to delete the extension method from our code ❤️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix it directly in main.

This is actually why I'm working on this Dokka Plugin: https://github.com/nomisRev/AnkDokkaPlugin/pull/7/files
I think we can already do it today too with our current setup, but if we wrote tests instead of examples than we could catch these mistakes on CI as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the docs compile as part of the build, so this isn't an error as such. Just a logical bug.

* 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
Original file line number Diff line number Diff line change
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)
}
}
}
}