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
Draft
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
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
164 changes: 75 additions & 89 deletions lib/api/lib.api

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ available from Arrow.
# Package app.cash.quiver
Custom types (e.g. [`Outcome`](app.cash.quiver.Outcome))

# Package app.cash.quiver.continuations
# Package app.cash.quiver.effects
Continuations for working with custom types (e.g. [`Outcome`](app.cash.quiver.Outcome))

```kotlin
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()

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package app.cash.quiver.effects

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

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

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

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

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

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

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

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

suspend inline operator fun <E, A> invoke(crossinline f: suspend OutcomeEffectScope<E>.() -> A): Outcome<E, A> =
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.effects.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
17 changes: 14 additions & 3 deletions lib/src/test/kotlin/app/cash/quiver/extensions/MapTest.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package app.cash.quiver.extensions

import arrow.core.Either
import arrow.core.NonEmptyList
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 arrow.core.toNonEmptyListOrNull
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.filter
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.list
import io.kotest.property.arbitrary.map
import io.kotest.property.arrow.core.nonEmptyList
import io.kotest.property.checkAll
Expand Down Expand Up @@ -129,7 +132,7 @@ class MapTest : StringSpec({
}

"traverse Option short-circuits" {
checkAll(Arb.nonEmptyList(Arb.int())) { ints ->
checkAll(arbNel(Arb.int())) { ints ->
val acc = mutableListOf<Int>()
val evens = ints.quiverTraverse {
if (it % 2 == 0) {
Expand All @@ -143,4 +146,12 @@ class MapTest : StringSpec({
evens.fold({ Unit }) { it shouldBe ints }
}
}
})
}) {
companion object {
// TODO(hugom): remove in favour of Arb.nonEmptyList once kotest-extensions-arrow has been upgraded to arrow 2.0.0
private fun <A> arbNel(a: Arb<A>): Arb<NonEmptyList<A>> =
Arb.list(a).filter(List<A>::isNotEmpty).map {
it.toNonEmptyListOrNull() ?: throw IndexOutOfBoundsException("Empty list")
}
}
}
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