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

[Arrow 2.0] Effect without suspending shift #2797

Merged
merged 24 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ac1c7c5
Shift without suspend, inline all the rest
nomisRev Aug 16, 2022
a01c496
Do some clean-up, make API on par with Arrow 1.x.x
nomisRev Aug 16, 2022
0b97f6c
Add some extra methods
nomisRev Aug 16, 2022
2ff582d
Re-add Effect doc, and some missing APIs
nomisRev Aug 17, 2022
e9cbf6e
Fix error handlers signatures
nomisRev Aug 17, 2022
942313a
Add catch for Either
nomisRev Aug 17, 2022
b21b56b
Re-add full test specs
nomisRev Aug 17, 2022
4f0d629
Re-add some docs, and missing functions
nomisRev Aug 17, 2022
18e5b54
Update API files
nomisRev Aug 17, 2022
a5d40c4
Retrigger CI with updated API files
nomisRev Aug 17, 2022
f6cd770
Merge branch 'arrow-2' into effect-without-suspend-fold
nomisRev Aug 29, 2022
8353082
Rename catch to recover
nomisRev Aug 30, 2022
32ce205
Update API files
nomisRev Aug 30, 2022
5ccf0ab
Refactor attempt to catch, update knit
nomisRev Aug 30, 2022
d50e570
Merge branch 'effect-without-suspend-fold' of github.com:arrow-kt/arr…
nomisRev Aug 30, 2022
424820d
Update API files
nomisRev Aug 30, 2022
627747c
Retrigger CI
nomisRev Aug 30, 2022
04c4e0a
Rename Either.catch to recover, and add docs
nomisRev Aug 30, 2022
2f8ef0e
Update API files
nomisRev Aug 30, 2022
60edb5c
Rename JvmName catchOrThrow to catchReified
nomisRev Aug 31, 2022
cc8ac31
Merge branch 'effect-without-suspend-fold' of github.com:arrow-kt/arr…
nomisRev Aug 31, 2022
c1e7eed
apiDump & knit
nomisRev Aug 31, 2022
c4ac7ea
Make ShiftCancellationException private
nomisRev Aug 31, 2022
1319792
Merge remote-tracking branch 'origin/arrow-2' into effect-without-sus…
nomisRev Sep 1, 2022
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
691 changes: 245 additions & 446 deletions arrow-libs/core/arrow-core/api/arrow-core.api

Large diffs are not rendered by default.

152 changes: 49 additions & 103 deletions arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package arrow.core
import arrow.core.Either.Companion.resolve
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.continuations.Shift
import arrow.core.continuations.either
import arrow.core.continuations.ensure
import arrow.typeclasses.Monoid
import arrow.typeclasses.Semigroup
import kotlin.experimental.ExperimentalTypeInference
Expand Down Expand Up @@ -1229,6 +1232,32 @@ public sealed class Either<out A, out B> {
map { Unit }
}

/**
* Recover from [E] when encountering [Left].
* You can either return a new value of [A], or short-circuit by shifting with a value of [E2].
*
* ```kotlin
* import arrow.core.Either
* import arrow.core.left
* import arrow.core.recover
*
* object User
* object Error
*
* val error: Either<Error, User> = Error.left()
*
* val a: Either<Error, User> = error.recover { error -> User } // Either.Right(User)
* val b: Either<String, User> = error.recover { error -> shift("other-failure") } // Either.Left(other-failure)
* val c: Either<Nothing, User> = error.recover { error -> User } // Either.Right(User)
* ```
* <!--- KNIT example-either.kt -->
*/
public inline fun <E2, E, A> Either<E, A>.recover(recover: Shift<E2>.(E) -> A): Either<E2, A> =
when (this) {
is Right -> this
is Left -> either { recover(value) }
}

/**
* Binds the given function across [Right].
*
Expand Down Expand Up @@ -1477,22 +1506,13 @@ public inline fun <A> Any?.rightIfNull(default: () -> A): Either<A, Nothing?> =
* This is like `flatMap` for the exception.
*/
public inline fun <A, B, C> Either<A, B>.handleErrorWith(f: (A) -> Either<C, B>): Either<C, B> =
when (this) {
is Left -> f(this.value)
is Right -> this
}
recover { f(it).bind() }

public inline fun <A, B> Either<A, B>.handleError(f: (A) -> B): Either<A, B> =
when (this) {
is Left -> f(value).right()
is Right -> this
}
recover { f(it) }

public inline fun <A, B, C> Either<A, B>.redeem(fe: (A) -> C, fa: (B) -> C): Either<A, C> =
when (this) {
is Left -> fe(value).right()
is Right -> map(fa)
}
either { fa(bind()) }.recover { fe(it) }

public operator fun <A : Comparable<A>, B : Comparable<B>> Either<A, B>.compareTo(other: Either<A, B>): Int =
fold(
Expand Down Expand Up @@ -1540,49 +1560,25 @@ public fun <AA, A : AA, B> Either<A, B>.leftWiden(): Either<AA, B> =
this

public fun <A, B, C, D> Either<A, B>.zip(fb: Either<A, C>, f: (B, C) -> D): Either<A, D> =
flatMap { b ->
fb.map { c -> f(b, c) }
}
either { f(bind(), fb.bind()) }

public fun <A, B, C> Either<A, B>.zip(fb: Either<A, C>): Either<A, Pair<B, C>> =
flatMap { a ->
fb.map { b -> Pair(a, b) }
}
either { Pair(bind(), fb.bind()) }

public inline fun <A, B, C, D, E> Either<A, B>.zip(
c: Either<A, C>,
d: Either<A, D>,
map: (B, C, D) -> E
): Either<A, E> =
zip(
c,
d,
Right.unit,
Right.unit,
Right.unit,
Right.unit,
Right.unit,
Right.unit,
Right.unit
) { b, c, d, _, _, _, _, _, _, _ -> map(b, c, d) }
either { map(bind(), c.bind(), d.bind()) }

public inline fun <A, B, C, D, E, F> Either<A, B>.zip(
c: Either<A, C>,
d: Either<A, D>,
e: Either<A, E>,
map: (B, C, D, E) -> F
): Either<A, F> =
zip(
c,
d,
e,
Right.unit,
Right.unit,
Right.unit,
Right.unit,
Right.unit,
Right.unit
) { b, c, d, e, _, _, _, _, _, _ -> map(b, c, d, e) }
either { map(bind(), c.bind(), d.bind(), e.bind()) }

public inline fun <A, B, C, D, E, F, G> Either<A, B>.zip(
c: Either<A, C>,
Expand All @@ -1591,17 +1587,7 @@ public inline fun <A, B, C, D, E, F, G> Either<A, B>.zip(
f: Either<A, F>,
map: (B, C, D, E, F) -> G
): Either<A, G> =
zip(
c,
d,
e,
f,
Right.unit,
Right.unit,
Right.unit,
Right.unit,
Right.unit
) { b, c, d, e, f, _, _, _, _, _ -> map(b, c, d, e, f) }
either { map(bind(), c.bind(), d.bind(), e.bind(), f.bind()) }

public inline fun <A, B, C, D, E, F, G, H> Either<A, B>.zip(
c: Either<A, C>,
Expand All @@ -1611,16 +1597,7 @@ public inline fun <A, B, C, D, E, F, G, H> Either<A, B>.zip(
g: Either<A, G>,
map: (B, C, D, E, F, G) -> H
): Either<A, H> =
zip(c, d, e, f, g, Right.unit, Right.unit, Right.unit, Right.unit) { b, c, d, e, f, g, _, _, _, _ ->
map(
b,
c,
d,
e,
f,
g
)
}
either { map(bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind()) }

public inline fun <A, B, C, D, E, F, G, H, I> Either<A, B>.zip(
c: Either<A, C>,
Expand All @@ -1631,17 +1608,7 @@ public inline fun <A, B, C, D, E, F, G, H, I> Either<A, B>.zip(
h: Either<A, H>,
map: (B, C, D, E, F, G, H) -> I
): Either<A, I> =
zip(c, d, e, f, g, h, Right.unit, Right.unit, Right.unit) { b, c, d, e, f, g, h, _, _, _ ->
map(
b,
c,
d,
e,
f,
g,
h
)
}
either { map(bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind()) }

public inline fun <A, B, C, D, E, F, G, H, I, J> Either<A, B>.zip(
c: Either<A, C>,
Expand All @@ -1653,7 +1620,7 @@ public inline fun <A, B, C, D, E, F, G, H, I, J> Either<A, B>.zip(
i: Either<A, I>,
map: (B, C, D, E, F, G, H, I) -> J
): Either<A, J> =
zip(c, d, e, f, g, h, i, Right.unit, Right.unit) { b, c, d, e, f, g, h, i, _, _ -> map(b, c, d, e, f, g, h, i) }
either { map(bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind(), i.bind()) }

public inline fun <A, B, C, D, E, F, G, H, I, J, K> Either<A, B>.zip(
c: Either<A, C>,
Expand All @@ -1666,7 +1633,7 @@ public inline fun <A, B, C, D, E, F, G, H, I, J, K> Either<A, B>.zip(
j: Either<A, J>,
map: (B, C, D, E, F, G, H, I, J) -> K
): Either<A, K> =
zip(c, d, e, f, g, h, i, j, Right.unit) { b, c, d, e, f, g, h, i, j, _ -> map(b, c, d, e, f, g, h, i, j) }
either { map(bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind(), i.bind(), j.bind()) }

public inline fun <A, B, C, D, E, F, G, H, I, J, K, L> Either<A, B>.zip(
c: Either<A, C>,
Expand All @@ -1679,28 +1646,9 @@ public inline fun <A, B, C, D, E, F, G, H, I, J, K, L> Either<A, B>.zip(
j: Either<A, J>,
k: Either<A, K>,
map: (B, C, D, E, F, G, H, I, J, K) -> L
): Either<A, L> =
flatMap { bb ->
c.flatMap { cc ->
d.flatMap { dd ->
e.flatMap { ee ->
f.flatMap { ff ->
g.flatMap { gg ->
h.flatMap { hh ->
i.flatMap { ii ->
j.flatMap { jj ->
k.map { kk ->
map(bb, cc, dd, ee, ff, gg, hh, ii, jj, kk)
}
}
}
}
}
}
}
}
}
}
): Either<A, L> = either {
map(bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind(), i.bind(), j.bind(), k.bind())
}

public fun <A, B> Either<A, B>.replicate(n: Int, MB: Monoid<B>): Either<A, B> =
if (n <= 0) MB.empty().right()
Expand All @@ -1712,16 +1660,14 @@ public fun <A, B> Either<A, B>.replicate(n: Int, MB: Monoid<B>): Either<A, B> =
}

public inline fun <A, B> Either<A, B>.ensure(error: () -> A, predicate: (B) -> Boolean): Either<A, B> =
when (this) {
is Right -> if (predicate(this.value)) this else error().left()
is Left -> this
either {
val b = bind()
ensure(predicate(b), error)
b
}

public inline fun <A, B, C, D> Either<A, B>.redeemWith(fa: (A) -> Either<C, D>, fb: (B) -> Either<C, D>): Either<C, D> =
when (this) {
is Left -> fa(this.value)
is Right -> fb(this.value)
}
either { fold({ fa(it).bind() }, { fb(it).bind() }) }

public fun <A, B> Either<A, Iterable<B>>.sequence(): List<Either<A, B>> =
traverse(::identity)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
@file:JvmMultifileClass
@file:JvmName("Effect")
@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class)

package arrow.core.continuations

import arrow.core.Either
import arrow.core.EmptyValue
import arrow.core.Ior
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.identity
import arrow.typeclasses.Semigroup
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmInline
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

public inline fun <E, A> either(@BuilderInference block: Shift<E>.() -> A): Either<E, A> =
Copy link
Member

Choose a reason for hiding this comment

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

These impls are so much nicer and simpler now 👏

fold({ block.invoke(this) }, { Either.Left(it) }, { Either.Right(it) })

public inline fun <A> nullable(block: NullableShift.() -> A): A? =
fold({ block(NullableShift(this)) }, { null }, ::identity)

public inline fun <A> result(action: ResultShift.() -> A): Result<A> =
fold({ action(ResultShift(this)) }, Result.Companion::failure, Result.Companion::success)

public inline fun <A> option(action: OptionShift.() -> A): Option<A> =
fold({ action(OptionShift(this)) }, ::identity, ::Some)

public inline fun <E, A> ior(semigroup: Semigroup<E>, @BuilderInference action: IorShift<E>.() -> A): Ior<E, A> =
fold({ IorShift(semigroup, this).invoke(action) }, { Ior.Left(it) }, ::identity)

@JvmInline
public value class NullableShift(private val cont: Shift<Nothing?>) : Shift<Nothing?> {
@EffectDSL
public fun ensure(value: Boolean): Unit = ensure(value) { null }
override fun <B> shift(r: Nothing?): B = cont.shift(r)
public fun <B> Option<B>.bind(): B = bind { shift(null) }

public fun <B> B?.bind(): B {
contract { returns() implies (this@bind != null) }
return this ?: shift(null)
}

public fun <B> ensureNotNull(value: B?): B {
contract { returns() implies (value != null) }
return ensureNotNull(value) { null }
}
}

@JvmInline
public value class ResultShift(private val cont: Shift<Throwable>) : Shift<Throwable> {
raulraja marked this conversation as resolved.
Show resolved Hide resolved
override fun <B> shift(r: Throwable): B = cont.shift(r)
public fun <B> Result<B>.bind(): B = fold(::identity) { shift(it) }
}

@JvmInline
public value class OptionShift(private val cont: Shift<None>) : Shift<None> {
override fun <B> shift(r: None): B = cont.shift(r)
public fun <B> Option<B>.bind(): B = bind { shift(None) }
public fun ensure(value: Boolean): Unit = ensure(value) { None }

public fun <B> ensureNotNull(value: B?): B {
contract { returns() implies (value != null) }
return ensureNotNull(value) { None }
}
}

public class IorShift<E> @PublishedApi internal constructor(semigroup: Semigroup<E>, private val effect: Shift<E>) :
Copy link
Member

Choose a reason for hiding this comment

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

These may be an exception to what I said above, but it's also not a value class since it takes more than one arg.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sam issue, it can be implemented for StateShift<State, R> if we have such a type. (Local prototype, shared with you on Slack). Can share gist here for everyone that is interested, otherwise I'll raise it as a follow-up PR.

Copy link
Member

Choose a reason for hiding this comment

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

Can you send in a gist link, please? 😄

Shift<E>, Semigroup<E> by semigroup {

// TODO this is a mess...
@PublishedApi
internal var leftState: AtomicRef<Any?> = AtomicRef(EmptyValue)
override fun <B> shift(r: E): B = effect.shift(combine(r))

public fun <B> Ior<E, B>.bind(): B =
when (this) {
is Ior.Left -> shift(value)
is Ior.Right -> value
is Ior.Both -> {
combine(leftValue)
rightValue
}
}

@PublishedApi
internal inline operator fun <A> invoke(action: IorShift<E>.() -> A): Ior<E, A> {
val res = action(this)
val leftState = leftState.get()
return if (leftState === EmptyValue) Ior.Right(res)
else Ior.Both(EmptyValue.unbox(leftState), res)
}

@Suppress("UNCHECKED_CAST")
private fun combine(other: E): E =
leftState.updateAndGet { state ->
if (state === EmptyValue) other else EmptyValue.unbox<E>(state).combine(other)
} as E
}
Loading