Skip to content
This repository has been archived by the owner on Feb 24, 2021. It is now read-only.

Suspend Validated.fx implementation #131

Merged
merged 21 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from 5 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
182 changes: 124 additions & 58 deletions arrow-core-data/src/main/kotlin/arrow/core/Validated.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import arrow.higherkind
import arrow.typeclasses.Applicative
import arrow.typeclasses.Semigroup
import arrow.typeclasses.Show
import kotlin.coroutines.Continuation
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn

typealias ValidatedNel<E, A> = Validated<Nel<E>, A>
typealias Valid<A> = Validated.Valid<A>
Expand Down Expand Up @@ -133,6 +137,33 @@ typealias Invalid<E> = Validated.Invalid<E>
* //sampleEnd
* ```
*
* But, as you can see, the parser runs sequentially: first tries to get the map value and then tries to read it.
aballano marked this conversation as resolved.
Show resolved Hide resolved
* This is easily translated to an Fx block:
aballano marked this conversation as resolved.
Show resolved Hide resolved
*
* ```kotlin:ank
* import arrow.core.None
* import arrow.core.Option
* import arrow.core.Some
* import arrow.core.Validated
* import arrow.core.valid
* import arrow.core.invalid
* import arrow.core.fx
*
* //sampleStart
* data class Config(val map: Map<String, String>) {
* suspend fun <A> parse(read: Read<A>, key: String) = Validated.fx<ConfigError, A> {
* val value = Validated.fromNullable(map[key]) {
* ConfigError.MissingConfig(key)
* }.bind()
* val readVal = Validated.fromOption(read.read(value)) {
* ConfigError.ParseConfig(key)
* }.bind()
* readVal
* }
* }
* //sampleEnd
* ```
*
* Everything is in place to write the parallel validator. Recall that we can only do parallel
* validation if each piece is independent. How do we ensure the data is independent? By
* asking for all of it up front. Let's start with two pieces of data.
Expand Down Expand Up @@ -161,7 +192,7 @@ typealias Invalid<E> = Validated.Invalid<E>
* that turns any `Validated<E, A>` value to a `Validated<NonEmptyList<E>, A>`. Additionally, the
* type alias `ValidatedNel<E, A>` is provided.
*
* Time to parse.
* Time to validate:
*
* ```kotlin:ank
* import arrow.core.NonEmptyList
Expand Down Expand Up @@ -190,6 +221,8 @@ typealias Invalid<E> = Validated.Invalid<E>
* import arrow.core.Validated
* import arrow.core.valid
* import arrow.core.invalid
* import arrow.core.fx
* import arrow.core.NonEmptyList
*
* data class ConnectionParams(val url: String, val port: Int)
*
Expand Down Expand Up @@ -217,29 +250,28 @@ typealias Invalid<E> = Validated.Invalid<E>
* }
*
* data class Config(val map: Map<String, String>) {
* fun <A> parse(read: Read<A>, key: String): Validated<ConfigError, A> {
* val v = Option.fromNullable(map[key])
* return when (v) {
* is Some ->
* when (val s = read.read(v.t)) {
* is Some -> s.t.valid()
* is None -> ConfigError.ParseConfig(key).invalid()
* }
* is None -> Validated.Invalid(ConfigError.MissingConfig(key))
* suspend fun <A> parse(read: Read<A>, key: String) = Validated.fx<ConfigError, A> {
* val value = Validated.fromNullable(map[key]) {
* ConfigError.MissingConfig(key)
* }.bind()
* val readVal = Validated.fromOption(read.read(value)) {
* ConfigError.ParseConfig(key)
* }.bind()
* readVal
* }
* }
* }
*
* fun <E, A, B, C> parallelValidate(v1: Validated<E, A>, v2: Validated<E, B>, f: (A, B) -> C): Validated<E, C> {
* return when {
* fun <E, A, B, C> parallelValidate
* (v1: Validated<E, A>, v2: Validated<E, B>, f: (A, B) -> C): Validated<NonEmptyList<E>, C> =
* when {
* v1 is Validated.Valid && v2 is Validated.Valid -> Validated.Valid(f(v1.a, v2.a))
* v1 is Validated.Valid && v2 is Validated.Invalid -> v2
* v1 is Validated.Invalid && v2 is Validated.Valid -> v1
* v1 is Validated.Invalid && v2 is Validated.Invalid -> TODO()
* else -> TODO()
* v1 is Validated.Valid && v2 is Validated.Invalid -> v2.toValidatedNel()
* v1 is Validated.Invalid && v2 is Validated.Valid -> v1.toValidatedNel()
* v1 is Validated.Invalid && v2 is Validated.Invalid -> Validated.Invalid(NonEmptyList(v1.e, listOf(v2.e)))
* else -> throw IllegalStateException("Not possible value")
Copy link
Member

Choose a reason for hiding this comment

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

Instead of Throw, we could return an Invalid with it.

Copy link
Member Author

Choose a reason for hiding this comment

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

But that's the impossible case, does it really matter? Actually I was looking for if there was an operator that does this already and doesn't leave the burden of writing this to the dev but couldn't see any

Copy link
Member

Choose a reason for hiding this comment

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

Isn't it exhaustive then?

Copy link
Member Author

Choose a reason for hiding this comment

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

It cannot be because we're checking 2 elements

Copy link
Member

@pakoito pakoito May 28, 2020

Choose a reason for hiding this comment

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

we need pattern matching weep. I'd still use Invalid for the sake of not promoting exceptions. Not a blocker tho.

Copy link
Member

@nomisRev nomisRev May 29, 2020

Choose a reason for hiding this comment

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

Yes, you can use Applicative for this.

Validated.applicative(NonEmptyList.semigroup<String>()).tupledN(
  1.valid(),
  "2".invalidNel(),
  "3".invalidNel()
) // Validated.Invalid(NonEmptyList("2", "3"))

You can always make these exhaustive by nesting when, which after a lot of back and forth have gone back to...

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, I just realized that we're in core-data and we don't have access to the extensions :/

What I did is the following:

  • Added a convenient nelApplicative function in Validated extensions
  • Suggest to use this approach instead of the custom function in docs, but without changing the examples code due to the import problem

Copy link
Member

Choose a reason for hiding this comment

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

It's just meh in general. It could be possible to rewrite it as two nested folds and some manual formatting.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of showing parallelValidate at all.. It promotes/shows code that no-one should ever write using Arrow.

We have a bunch of helpers in the docs module, any way we can provide instances there to make the desired snippets compile?

Copy link
Member Author

Choose a reason for hiding this comment

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

The only reason I like it is for the explanation vs a Validated Nel, which is also explained in there. I changed it to try to make it clear that this is just for the shake of the example

* }
* }
*
* suspend fun main() {
* //sampleStart
* val config = Config(mapOf("url" to "127.0.0.1", "port" to "1337"))
*
Expand All @@ -248,7 +280,6 @@ typealias Invalid<E> = Validated.Invalid<E>
* config.parse(Read.intRead, "port")
* ) { url, port -> ConnectionParams(url, port) }
* //sampleEnd
* fun main() {
* println("valid = $valid")
* }
* ```
Expand All @@ -263,6 +294,8 @@ typealias Invalid<E> = Validated.Invalid<E>
* import arrow.core.Validated
* import arrow.core.valid
* import arrow.core.invalid
* import arrow.core.NonEmptyList
* import arrow.core.fx
*
* data class ConnectionParams(val url: String, val port: Int)
*
Expand Down Expand Up @@ -290,38 +323,36 @@ typealias Invalid<E> = Validated.Invalid<E>
* }
*
* data class Config(val map: Map<String, String>) {
* fun <A> parse(read: Read<A>, key: String): Validated<ConfigError, A> {
* val v = Option.fromNullable(map[key])
* return when (v) {
* is Some ->
* when (val s = read.read(v.t)) {
* is Some -> s.t.valid()
* is None -> ConfigError.ParseConfig(key).invalid()
* }
* is None -> Validated.Invalid(ConfigError.MissingConfig(key))
* suspend fun <A> parse(read: Read<A>, key: String) = Validated.fx<ConfigError, A> {
* val value = Validated.fromNullable(map[key]) {
* ConfigError.MissingConfig(key)
* }.bind()
* val readVal = Validated.fromOption(read.read(value)) {
* ConfigError.ParseConfig(key)
* }.bind()
* readVal
* }
* }
* }
*
* fun <E, A, B, C> parallelValidate(v1: Validated<E, A>, v2: Validated<E, B>, f: (A, B) -> C): Validated<E, C> {
* return when {
* fun <E, A, B, C> parallelValidate
* (v1: Validated<E, A>, v2: Validated<E, B>, f: (A, B) -> C): Validated<NonEmptyList<E>, C> =
* when {
* v1 is Validated.Valid && v2 is Validated.Valid -> Validated.Valid(f(v1.a, v2.a))
* v1 is Validated.Valid && v2 is Validated.Invalid -> v2
* v1 is Validated.Invalid && v2 is Validated.Valid -> v1
* v1 is Validated.Invalid && v2 is Validated.Invalid -> TODO()
* else -> TODO()
* v1 is Validated.Valid && v2 is Validated.Invalid -> v2.toValidatedNel()
* v1 is Validated.Invalid && v2 is Validated.Valid -> v1.toValidatedNel()
* v1 is Validated.Invalid && v2 is Validated.Invalid -> Validated.Invalid(NonEmptyList(v1.e, listOf(v2.e)))
* else -> throw IllegalStateException("Not possible value")
* }
* }
*
* suspend fun main() {
* //sampleStart
* val config = Config(mapOf("url" to "127.0.0.1", "port" to "not a number"))
* val config = Config(mapOf("wrong field" to "127.0.0.1", "port" to "not a number"))
*
* val valid = parallelValidate(
* config.parse(Read.stringRead, "url"),
* config.parse(Read.intRead, "port")
* ) { url, port -> ConnectionParams(url, port) }
* //sampleEnd
* fun main() {
* //sampleEnd
* println("valid = $valid")
* }
* ```
Expand All @@ -343,6 +374,7 @@ typealias Invalid<E> = Validated.Invalid<E>
* import arrow.core.Validated
* import arrow.core.valid
* import arrow.core.invalid
* import arrow.core.fx
*
* abstract class Read<A> {
* abstract fun read(s: String): Option<A>
Expand All @@ -363,18 +395,17 @@ typealias Invalid<E> = Validated.Invalid<E>
* }
*
* data class Config(val map: Map<String, String>) {
* fun <A> parse(read: Read<A>, key: String): Validated<ConfigError, A> {
* val v = Option.fromNullable(map[key])
* return when (v) {
* is Some ->
* when (val s = read.read(v.t)) {
* is Some -> s.t.valid()
* is None -> ConfigError.ParseConfig(key).invalid()
* }
* is None -> Validated.Invalid(ConfigError.MissingConfig(key))
* suspend fun <A> parse(read: Read<A>, key: String) = Validated.fx<ConfigError, A> {
* val value = Validated.fromNullable(map[key]) {
Copy link
Member

Choose a reason for hiding this comment

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

Neat

* ConfigError.MissingConfig(key)
* }.bind()
* val readVal = Validated.fromOption(read.read(value)) {
Copy link
Member

Choose a reason for hiding this comment

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

Great! Good use of both APIs

* ConfigError.ParseConfig(key)
* }.bind()
* readVal
* }
* }
* }
*
* sealed class ConfigError {
* data class MissingConfig(val field: String) : ConfigError()
* data class ParseConfig(val field: String) : ConfigError()
Expand All @@ -388,11 +419,11 @@ typealias Invalid<E> = Validated.Invalid<E>
*
* val config = Config(mapOf("house_number" to "-42"))
*
* val houseNumber = config.parse(Read.intRead, "house_number").withEither { either ->
* either.flatMap { positive("house_number", it) }
* }
* suspend fun main() {
* val houseNumber = config.parse(Read.intRead, "house_number").withEither { either ->
* either.flatMap { positive("house_number", it) }
aballano marked this conversation as resolved.
Show resolved Hide resolved
* }
* //sampleEnd
* fun main() {
* println(houseNumber)
* }
*
Expand Down Expand Up @@ -618,19 +649,26 @@ sealed class Validated<out E, out A> : ValidatedOf<E, A> {
fun <A> fromTry(t: Try<A>): Validated<Throwable, A> = t.fold({ Invalid(it) }, { Valid(it) })

/**
* Converts an `Either<A, B>` to an `Validated<A, B>`.
* Converts an `Either<E, A>` to a `Validated<E, A>`.
*/
fun <E, A> fromEither(e: Either<E, A>): Validated<E, A> = e.fold({ Invalid(it) }, { Valid(it) })

/**
* Converts an `Option<B>` to an `Validated<A, B>`, where the provided `ifNone` values is returned on
* the invalid of the `Validated` when the specified `Option` is `None`.
* Converts an `Option<A>` to a `Validated<E, A>`, where the provided `ifNone` output value is returned as invalid
aballano marked this conversation as resolved.
Show resolved Hide resolved
* when the specified `Option` is `None`.
*/
fun <E, A> fromOption(o: Option<A>, ifNone: () -> E): Validated<E, A> =
o.fold(
{ Invalid(ifNone()) },
{ Valid(it) }
)

/**
* Converts a nullable `A?` to a `Validated<E, A>`, where the provided `ifNull` output value is returned as invalid
* when the specified value is null.
*/
fun <E, A> fromNullable(value: A?, ifNull: () -> E): Validated<E, A> =
value?.let(::Valid) ?: Invalid(ifNull())
}

fun show(SE: Show<E>, SA: Show<A>): String = fold({
Expand Down Expand Up @@ -693,8 +731,9 @@ sealed class Validated<out E, out A> : ValidatedOf<E, A> {
fun <EE, B> withEither(f: (Either<E, A>) -> Either<EE, B>): Validated<EE, B> = fromEither(f(toEither()))

/**
* Validated is a [functor.Bifunctor], this method applies one of the
* given functions.
* From [arrow.typeclasses.Bifunctor], maps both types of this Validated.
*
* Apply a function to an Invalid or Valid value, returning a new Invalid or Valid value respectively.
*/
fun <EE, AA> bimap(fe: (E) -> EE, fa: (A) -> AA): Validated<EE, AA> = fold({ Invalid(fe(it)) }, { Valid(fa(it)) })

Expand Down Expand Up @@ -842,3 +881,30 @@ fun <A> A.validNel(): ValidatedNel<Nothing, A> =

fun <E> E.invalidNel(): ValidatedNel<E, Nothing> =
Validated.invalidNel(this)

suspend fun <E, A> Validated.Companion.fx(c: suspend ValidatedContinuation<E, A>.() -> A): Validated<E, A> =
suspendCoroutineUninterceptedOrReturn sc@{ cont ->
val continuation = ValidatedContinuation(cont as Continuation<ValidatedOf<E, A>>)
val wrapReturn: suspend ValidatedContinuation<E, A>.() -> Validated<E, A> = { c().valid() }

// Returns Validated `Validated<A, B>` or `COROUTINE_SUSPENDED`
val x: Any? = try {
wrapReturn.startCoroutineUninterceptedOrReturn(continuation, continuation)
} catch (e: Throwable) {
if (e is SuspendMonadContinuation.ShortCircuit) Invalid(e.e as E)
else throw e
}

return@sc if (x == COROUTINE_SUSPENDED) continuation.getResult()
else x as Validated<E, A>
}

class ValidatedContinuation<E, A>(
parent: Continuation<ValidatedOf<E, A>>
) : SuspendMonadContinuation<ValidatedPartialOf<E>, A>(parent) {
override suspend fun <A> Kind<ValidatedPartialOf<E>, A>.bind(): A =
fix().fold({ e -> throw ShortCircuit(e) }, ::identity)

override fun ShortCircuit.recover(): Kind<ValidatedPartialOf<E>, A> =
Invalid(e as E)
}
Loading