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

WIP: upgrade to arrow 2.0 #83

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
arrow = "1.2.4"
arrow = "2.0.0-alpha.2"
dokka = "1.9.20"
junit = "5.10.2"
kotest = "5.8.1"
Expand Down
10 changes: 0 additions & 10 deletions lib/src/main/kotlin/app/cash/quiver/Outcome.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ import arrow.core.Either.Right
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.Validated
import arrow.core.flatMap
import arrow.core.getOrElse
import arrow.core.identity
import arrow.core.left
import arrow.core.right
import arrow.core.some
import arrow.core.valid
import app.cash.quiver.extensions.orThrow
import app.cash.quiver.raise.OutcomeRaise
import app.cash.quiver.raise.outcome
Expand Down Expand Up @@ -292,12 +290,6 @@ fun <E, EE, A> Outcome<E, Either<EE, A>>.sequence(): Either<EE, Outcome<E, A>> =
is Present -> this.value.map(::Present)
}

fun <E, EE, A> Outcome<E, Validated<EE, A>>.sequence(): Validated<EE, Outcome<E, A>> = when (this) {
Absent -> Absent.valid()
is Failure -> this.valid()
is Present -> this.value.map(::Present)
}

fun <E, A> Iterable<Outcome<E, A>>.sequence(): Outcome<E, List<A>> =
outcome { map { it.bind() } }

Expand All @@ -307,5 +299,3 @@ inline fun <E, A, B> Outcome<E, A>.traverse(f: (A) -> Option<B>): Option<Outcome
@OptIn(ExperimentalTypeInference::class)
@OverloadResolutionByLambdaReturnType
inline fun <E, EE, A, B> Outcome<E, A>.traverse(f: (A) -> Either<EE, B>): Either<EE, Outcome<E, B>> = this.map(f).sequence()
inline fun <E, EE, A, B> Outcome<E, A>.traverse(f: (A) -> Validated<EE, B>): Validated<EE, Outcome<E, B>> =
this.map(f).sequence()
Original file line number Diff line number Diff line change
@@ -1,58 +1,42 @@
@file:Suppress("DEPRECATION")

package app.cash.quiver.continuations

import arrow.core.Either
import arrow.core.continuations.EagerEffectScope
import arrow.core.continuations.EffectScope
import arrow.core.continuations.eagerEffect
import arrow.core.continuations.effect
import arrow.core.left
import arrow.core.merge
import arrow.core.right
import app.cash.quiver.Absent
import app.cash.quiver.Failure
import app.cash.quiver.Outcome
import app.cash.quiver.Present
import arrow.core.Either
import arrow.core.left
import arrow.core.merge
import arrow.core.raise.Raise
import arrow.core.raise.eagerEffect
import arrow.core.raise.effect
import arrow.core.raise.fold
import arrow.core.right

// TODO(hugom): move this into an effect package, instead of continuations to align with arrow (?)
@JvmInline
value class OutcomeEagerEffectScope<E>(private val cont: EagerEffectScope<Either<Failure<E>, Absent>>) :
EagerEffectScope<Either<Failure<E>, Absent>> {
value class OutcomeEffectScope<E>(private val cont: Raise<Either<Failure<E>, Absent>>) :
Raise<Either<Failure<E>, Absent>> {

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be removed, I contributed a Raise impl last year in the raise package.

https://github.com/cashapp/quiver/blob/main/lib/src/main/kotlin/app/cash/quiver/raise/OutcomeBuilder.kt

Copy link
Member Author

Choose a reason for hiding this comment

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

Awesome! I'll look at migrating some of our existing codebases over to OutcomeRaise and see how it works out before removing 👍🏻

suspend fun <B> Outcome<E, B>.bind(): B =
when (this) {
is Absent -> shift(Absent.right())
is Failure -> shift(this.left())
Absent -> raise(Absent.right())
is Failure -> raise(this.left())
is Present -> value
}

@Suppress("ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL")
override suspend fun <B> shift(r: Either<Failure<E>, Absent>): B = cont.shift(r)
}

@JvmInline
value class OutcomeEffectScope<E>(private val cont: EffectScope<Either<Failure<E>, Absent>>) :
EffectScope<Either<Failure<E>, Absent>> {

public suspend fun <B> Outcome<E, B>.bind(): B =
when (this) {
Absent -> shift(Absent.right())
is Failure -> shift(this.left())
is Present -> value
}

override suspend fun <B> shift(r: Either<Failure<E>, Absent>): B =
cont.shift(r)
override fun raise(r: Either<Failure<E>, Absent>): Nothing = cont.raise(r)
}

@Suppress("ClassName")
object outcome {
inline fun <E, A> eager(crossinline f: suspend OutcomeEagerEffectScope<E>.() -> A): Outcome<E, A> =
inline fun <E, A> eager(crossinline f: OutcomeEffectScope<E>.() -> A): Outcome<E, A> =
eagerEffect {
@Suppress("ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL")
f(OutcomeEagerEffectScope(this))
f.invoke(OutcomeEffectScope<E>(this))
}.fold({ it.merge() }, ::Present)

suspend inline operator fun <E, A> invoke(crossinline f: suspend OutcomeEffectScope<E>.() -> A): Outcome<E, A> =
effect { f(OutcomeEffectScope(this)) }.fold({ it.merge() }, ::Present)
effect {
f.invoke(OutcomeEffectScope<E>(this))
}.fold({ it.merge() }, ::Present)
}
8 changes: 6 additions & 2 deletions lib/src/main/kotlin/app/cash/quiver/extensions/Option.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import arrow.core.Either
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.ValidatedNel
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.nonEmptyListOf
import arrow.core.right
import kotlin.experimental.ExperimentalTypeInference
import app.cash.quiver.extensions.traverse as quiverTraverse

Expand Down Expand Up @@ -38,7 +39,10 @@ inline fun <A> Option<A>.forEach(f: (A) -> Unit) {
* If it's a None, will return a Nel of the error function passed in
*/
inline fun <T, E> Option<T>.toValidatedNel(error: () -> E): ValidatedNel<E, T> =
ValidatedNel.fromOption(this) { nonEmptyListOf(error()) }
this.fold(
{ nonEmptyListOf(error()).left() },
{ it.right() }
)

/**
* Map some to Unit. This restores `.void()` which was deprecated by Arrow.
Expand Down
31 changes: 12 additions & 19 deletions lib/src/main/kotlin/app/cash/quiver/extensions/Validated.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
@file:Suppress("TYPEALIAS_EXPANSION_DEPRECATION", "DEPRECATION")

package app.cash.quiver.extensions

import arrow.core.Either
import arrow.core.ValidatedNel
import arrow.core.NonEmptyList
import arrow.core.flatMap
import arrow.core.getOrElse
import arrow.core.invalidNel
import arrow.core.left
import arrow.core.nonEmptyListOf
import arrow.core.right
import arrow.core.validNel
import arrow.core.zip

typealias ValidatedNel<E, A> = Either<NonEmptyList<E>, A>

/**
* Turns your Validated List into an Either, but will throw an exception in the Left hand case.
*/
fun <E, A> ValidatedNel<E, A>.attemptValidated(): Either<Throwable, A> =
this.toEither()
.mapLeft { errors -> RuntimeException(errors.toString()) }
this.mapLeft { errors -> RuntimeException(errors.toString()) }

/**
* Given a predicate and an error generating function return either the original value in a ValidNel if the
Expand All @@ -28,7 +25,7 @@ fun <E, A> ValidatedNel<E, A>.attemptValidated(): Either<Throwable, A> =
*
*/
inline fun <ERR, A> A.validate(predicate: (A) -> Boolean, error: (A) -> ERR): ValidatedNel<ERR, A> =
if (predicate(this)) this.validNel() else error(this).invalidNel()
if (predicate(this)) this.right() else nonEmptyListOf(error(this)).left()

/**
* Given a predicate and an error generating function return either the original value in a Right if the
Expand All @@ -46,28 +43,24 @@ inline fun <ERR, A> A.validateEither(predicate: (A) -> Boolean, error: (A) -> ER
* to pair them. takeLeft will return the value of the left side iff both validations
* succeed.
*
* eg.
* eg:
*
* Valid("hi").takeLeft(Valid("mum")) == Valid("hi")
*/
fun <ERR, A> ValidatedNel<ERR, A>.takeLeft(other: ValidatedNel<ERR, A>): ValidatedNel<ERR, A> =
this.zip(other) { a, _ ->
a
}
this.zip(other) { a, _ -> a }

/**
* Often you have two validations that return the same thing, and you don't want necessarily
* to pair them. takeRight will return the value of the right side iff both validations
* succeed.
*
* eg.
* eg:
*
* Valid("hi").takeRight(Valid("mum")) == Valid("mum")
*/
fun <ERR, A> ValidatedNel<ERR, A>.takeRight(other: ValidatedNel<ERR, A>): ValidatedNel<ERR, A> =
this.zip(other) { _, b ->
b
}
this.zip(other) { _, b -> b }

/**
* Given a mapping function and an error message, return either the result of the function in a
Expand All @@ -77,7 +70,7 @@ inline fun <ERR, A, B> A.validateMap(
f: (A) -> Either<Throwable, B>,
error: (A, Throwable) -> ERR
): ValidatedNel<ERR, B> =
f(this).map { it.validNel() }.getOrElse { error(this, it).invalidNel() }
f(this).map { it.right() }.getOrElse { nonEmptyListOf(error(this, it)).left() }

/**
* The Validated type doesn't natively support flatMap because of the monad laws that it breaks. But this
Expand All @@ -93,4 +86,4 @@ inline fun <ERR, A, B> A.validateMap(
* result == ValidNel("$jackjack")
*/
inline fun <ERR, A, B> ValidatedNel<ERR, A>.concatMap(f: (A) -> ValidatedNel<ERR, B>): ValidatedNel<ERR, B> =
this.withEither { either -> either.flatMap { f(it).toEither() } }
this.flatMap { f(it) }
22 changes: 1 addition & 21 deletions lib/src/test/kotlin/app/cash/quiver/OutcomeTest.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
@file:Suppress("DEPRECATION")

package app.cash.quiver

import app.cash.quiver.arb.outcome
import app.cash.quiver.continuations.outcome
import app.cash.quiver.matchers.shouldBeAbsent
import app.cash.quiver.matchers.shouldBeFailure
import app.cash.quiver.matchers.shouldBePresent
import arrow.core.Either
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.Validated
import arrow.core.invalid
import arrow.core.left
import arrow.core.right
import arrow.core.some
import arrow.core.valid
import app.cash.quiver.continuations.outcome
import io.kotest.assertions.arrow.core.shouldBeInvalid
import io.kotest.assertions.arrow.core.shouldBeLeft
import io.kotest.assertions.arrow.core.shouldBeNone
import io.kotest.assertions.arrow.core.shouldBeRight
import io.kotest.assertions.arrow.core.shouldBeSome
import io.kotest.assertions.arrow.core.shouldBeValid
import io.kotest.assertions.fail
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.common.runBlocking
Expand Down Expand Up @@ -354,19 +347,6 @@ class OutcomeTest : StringSpec({
absent.sequence().shouldBeSome().shouldBeAbsent()
}

"Validated sequence/traverse" {
Present(1.valid()).sequence().shouldBeValid().shouldBePresent().shouldBe(1)
Present(1.valid()).sequence() shouldBe Present(1).traverse { a: Int -> a.valid() }

val bad: Outcome<String, Validated<String, Int>> = "bad".failure()
bad.sequence().shouldBeValid().shouldBeFailure().shouldBe("bad")

val absent: Outcome<String, Validated<String, Int>> = Absent
absent.sequence().shouldBeValid().shouldBeAbsent()

Present("bad".invalid()).sequence().shouldBeInvalid()
}

"List sequence/traverse" {
Present(listOf(1, 2, 3)).sequence().shouldBe(listOf(1.present(), 2.present(), 3.present()))
Present(listOf(1, 1)).sequence() shouldBe Present(1).traverse { a: Int -> listOf(a, a) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import arrow.core.Either
import arrow.core.None
import arrow.core.Some
import arrow.core.left
import arrow.core.maybe
import arrow.core.right
import arrow.core.some
import io.kotest.core.spec.style.StringSpec
Expand Down
13 changes: 13 additions & 0 deletions lib/src/test/kotlin/app/cash/quiver/extensions/OptionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import io.kotest.property.checkAll
import app.cash.quiver.extensions.traverse as quiverTraverse
import app.cash.quiver.extensions.traverseEither as quiverTraverseEither
import app.cash.quiver.extensions.ifPresent
import arrow.core.NonEmptyList
import io.kotest.assertions.arrow.core.shouldBeLeft
import io.kotest.assertions.arrow.core.shouldBeRight
import io.kotest.assertions.arrow.core.shouldHaveSize
import io.kotest.matchers.shouldHave
import io.kotest.matchers.types.shouldBeInstanceOf

class OptionTest : StringSpec({

Expand Down Expand Up @@ -79,6 +85,13 @@ class OptionTest : StringSpec({
sideEffectRun shouldBe false
}

"toValidatedNel converts an option to a ValidatedNel" {
val errorList = None.toValidatedNel { IllegalStateException("Empty!") }.shouldBeLeft()
errorList.first().shouldBeInstanceOf<IllegalStateException>()

Some(42).toValidatedNel { IllegalStateException("Invalid") }.shouldBeRight(42)
}

@Suppress("UNREACHABLE_CODE")
"traverse to iterable of None returns an empty list" {
None.quiverTraverse { listOf(it) } shouldBe emptyList()
Expand Down
Loading