Skip to content

Commit 41a15f4

Browse files
nomisRevaballano
andauthored
Either concrete non-suspend fx (#124)
* Suspend fx * Add concrete Either fx * Fix docs Co-authored-by: Alberto Ballano <[email protected]>
1 parent 3033051 commit 41a15f4

File tree

6 files changed

+120
-32
lines changed

6 files changed

+120
-32
lines changed

arrow-libs/core/arrow-core-data/src/main/kotlin/arrow/core/Either.kt

+27-22
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ import arrow.core.Either.Left
55
import arrow.core.Either.Right
66
import arrow.higherkind
77
import arrow.typeclasses.Show
8+
import arrow.typeclasses.suspended.BindSyntax
89
import kotlin.coroutines.Continuation
9-
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
10-
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
1110
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
1211

1312
/**
@@ -651,6 +650,7 @@ import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
651650
* import arrow.core.extensions.fx
652651
* import arrow.core.Either
653652
*
653+
* suspend fun main() {
654654
* val value =
655655
* //sampleStart
656656
* Either.fx<Int, Int> {
@@ -660,7 +660,6 @@ import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
660660
* a + b + c
661661
* }
662662
* //sampleEnd
663-
* fun main() {
664663
* println(value)
665664
* }
666665
* ```
@@ -901,6 +900,21 @@ sealed class Either<out A, out B> : EitherOf<A, B> {
901900
} catch (t: Throwable) {
902901
fe(t.nonFatalOrThrow()).left()
903902
}
903+
904+
fun <E, A> fx2(c: suspend EagerBind<EitherPartialOf<E>>.() -> A): Either<E, A> {
905+
val continuation: EitherContinuation<E, A> = EitherContinuation()
906+
return continuation.startCoroutineUninterceptedAndReturn {
907+
Right(c())
908+
} as Either<E, A>
909+
}
910+
911+
suspend fun <E, A> fx(c: suspend BindSyntax<EitherPartialOf<E>>.() -> A): Either<E, A> =
912+
suspendCoroutineUninterceptedOrReturn sc@{ cont ->
913+
val continuation = EitherSContinuation(cont as Continuation<EitherOf<E, A>>)
914+
continuation.startCoroutineUninterceptedOrReturn {
915+
Right(c())
916+
}
917+
}
904918
}
905919
}
906920

@@ -1102,29 +1116,20 @@ fun <A, B> EitherOf<A, B>.handleErrorWith(f: (A) -> EitherOf<A, B>): Either<A, B
11021116
}
11031117
}
11041118

1105-
suspend fun <E, A> Either.Companion.fx(c: suspend EitherContinuation<E, A>.() -> A): Either<E, A> =
1106-
suspendCoroutineUninterceptedOrReturn sc@{ cont ->
1107-
val continuation = EitherContinuation(cont as Continuation<EitherOf<E, A>>)
1108-
val wrapReturn: suspend EitherContinuation<E, A>.() -> Either<E, A> = { c().right() }
1109-
1110-
// Returns either `Either<A, B>` or `COROUTINE_SUSPENDED`
1111-
val x: Any? = try {
1112-
wrapReturn.startCoroutineUninterceptedOrReturn(continuation, continuation)
1113-
} catch (e: Throwable) {
1114-
if (e is SuspendMonadContinuation.ShortCircuit) Left(e.e as E)
1115-
else throw e
1116-
}
1117-
1118-
return@sc if (x == COROUTINE_SUSPENDED) continuation.getResult()
1119-
else x as Either<E, A>
1120-
}
1121-
1122-
class EitherContinuation<E, A>(
1119+
internal class EitherSContinuation<E, A>(
11231120
parent: Continuation<EitherOf<E, A>>
11241121
) : SuspendMonadContinuation<EitherPartialOf<E>, A>(parent) {
1122+
override fun ShortCircuit.recover(): Kind<EitherPartialOf<E>, A> =
1123+
Left(value as E)
1124+
11251125
override suspend fun <A> Kind<EitherPartialOf<E>, A>.bind(): A =
11261126
fix().fold({ e -> throw ShortCircuit(e) }, ::identity)
1127+
}
11271128

1129+
internal class EitherContinuation<E, A> : MonadContinuation<EitherPartialOf<E>, A>() {
11281130
override fun ShortCircuit.recover(): Kind<EitherPartialOf<E>, A> =
1129-
Left(e as E)
1131+
Left(value as E)
1132+
1133+
override suspend fun <A> Kind<EitherPartialOf<E>, A>.bind(): A =
1134+
fix().fold({ e -> throw ShortCircuit(e) }, ::identity)
11301135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package arrow.core
2+
3+
import arrow.Kind
4+
import arrow.typeclasses.suspended.BindSyntax
5+
import kotlin.coroutines.Continuation
6+
import kotlin.coroutines.CoroutineContext
7+
import kotlin.coroutines.EmptyCoroutineContext
8+
import kotlin.coroutines.RestrictsSuspension
9+
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
10+
11+
@RestrictsSuspension
12+
interface EagerBind<F> : BindSyntax<F>
13+
14+
@PublishedApi
15+
internal class ShortCircuit(val value: Any?) : RuntimeException(null, null) {
16+
override fun fillInStackTrace(): Throwable = this
17+
override fun toString(): String = "ShortCircuit($value)"
18+
}
19+
20+
@Suppress("UNCHECKED_CAST")
21+
internal abstract class MonadContinuation<F, A> : Continuation<Kind<F, A>>, EagerBind<F> {
22+
23+
abstract fun ShortCircuit.recover(): Kind<F, A>
24+
25+
override val context: CoroutineContext = EmptyCoroutineContext
26+
27+
protected lateinit var returnedMonad: Kind<F, A>
28+
29+
fun returnedMonad(): Kind<F, A> = returnedMonad
30+
31+
override fun resumeWith(result: Result<Kind<F, A>>) {
32+
result.fold({ returnedMonad = it }, { e ->
33+
if (e is ShortCircuit) {
34+
returnedMonad = e.recover()
35+
} else throw e
36+
})
37+
}
38+
39+
fun startCoroutineUninterceptedAndReturn(f: suspend EagerBind<F>.() -> Kind<F, A>): Any? =
40+
try {
41+
f.startCoroutineUninterceptedOrReturn(this, this) as Kind<F, A>
42+
} catch (e: Throwable) {
43+
if (e is ShortCircuit) e.recover()
44+
else throw e
45+
}
46+
}

arrow-libs/core/arrow-core-data/src/main/kotlin/arrow/core/SuspendingMonadContinuation.kt

+15-5
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@ import kotlin.coroutines.Continuation
88
import kotlin.coroutines.CoroutineContext
99
import kotlin.coroutines.EmptyCoroutineContext
1010
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
11+
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
1112
import kotlin.coroutines.resumeWithException
1213

1314
internal const val UNDECIDED = 0
1415
internal const val SUSPENDED = 1
1516

1617
@Suppress("UNCHECKED_CAST")
17-
abstract class SuspendMonadContinuation<F, A>(private val parent: Continuation<Kind<F, A>>) : Continuation<Kind<F, A>>, BindSyntax<F> {
18-
19-
class ShortCircuit(val e: Any?) : RuntimeException(null, null) {
20-
override fun fillInStackTrace(): Throwable = this
21-
}
18+
internal abstract class SuspendMonadContinuation<F, A>(
19+
private val parent: Continuation<Kind<F, A>>
20+
) : Continuation<Kind<F, A>>, BindSyntax<F> {
2221

2322
abstract fun ShortCircuit.recover(): Kind<F, A>
2423

@@ -74,4 +73,15 @@ abstract class SuspendMonadContinuation<F, A>(private val parent: Continuation<K
7473
else -> return decision
7574
}
7675
}
76+
77+
fun startCoroutineUninterceptedOrReturn(f: suspend SuspendMonadContinuation<F, A>.() -> Kind<F, A>): Any? =
78+
try {
79+
f.startCoroutineUninterceptedOrReturn(this, this)?.let {
80+
if (it == COROUTINE_SUSPENDED) getResult()
81+
else it
82+
}
83+
} catch (e: Throwable) {
84+
if (e is ShortCircuit) e.recover()
85+
else throw e
86+
}
7787
}

arrow-libs/core/arrow-core-data/src/main/kotlin/arrow/typeclasses/MonadContinuations.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ import kotlin.coroutines.resume
1212
@RestrictsSuspension
1313
interface MonadSyntax<F> : Monad<F>, BindSyntax<F>
1414

15-
open class MonadContinuation<F, A>(M: Monad<F>, override val context: CoroutineContext = EmptyCoroutineContext) :
16-
Continuation<Kind<F, A>>, Monad<F> by M, BindSyntax<F>, MonadSyntax<F> {
15+
open class MonadContinuation<F, A>(
16+
M: Monad<F>,
17+
override val context: CoroutineContext = EmptyCoroutineContext
18+
) : Continuation<Kind<F, A>>, Monad<F> by M, BindSyntax<F>, MonadSyntax<F> {
1719

1820
override fun resume(value: Kind<F, A>) {
1921
returnedMonad = value
2022
}
2123

2224
@Suppress("UNCHECKED_CAST")
2325
override fun resumeWithException(exception: Throwable) {
24-
throw exception
26+
throw exception
2527
}
2628

2729
protected lateinit var returnedMonad: Kind<F, A>

arrow-libs/core/arrow-core-data/src/test/kotlin/arrow/core/EitherTest.kt

+23
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,29 @@ class EitherTest : UnitSpec() {
212212
}
213213
}
214214

215+
"non-suspended Either.fx can bind immediate values" {
216+
forAll(Gen.either(Gen.string(), Gen.int())) { either ->
217+
Either.fx2<String, Int> {
218+
val res = !either
219+
res
220+
} == either
221+
}
222+
}
223+
224+
"non-suspended Either.fx can safely handle immediate exceptions" {
225+
forAll(Gen.int(), Gen.throwable()) { i: Int, exception ->
226+
shouldThrow<Throwable> {
227+
Either.fx2<String, Int> {
228+
val res = !Either.Right(i)
229+
throw exception
230+
res
231+
}
232+
233+
fail("It should never reach here. Either.fx should've thrown $exception")
234+
} == exception
235+
}
236+
}
237+
215238
"suspended Either.fx can bind immediate values" {
216239
Gen.either(Gen.string(), Gen.int())
217240
.random()

arrow-libs/core/arrow-docs/docs/patterns/errorhandling/README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ fun getKnife(): Either<KnifeIsDull, Knife> = Right(Knife)
236236
fun lunch(knife: Knife, food: Lettuce): Either<InsufficientAmountOfLettuce, Salad> = Left(InsufficientAmountOfLettuce(5))
237237
238238
//sampleStart
239-
fun prepareEither(): Either<CookingException, Salad> =
239+
suspend fun prepareEither(): Either<CookingException, Salad> =
240240
Either.fx {
241241
val lettuce = takeFoodFromRefrigerator().bind()
242242
val knife = getKnife().bind()
@@ -245,7 +245,9 @@ fun prepareEither(): Either<CookingException, Salad> =
245245
}
246246
//sampleEnd
247247
248-
prepareEither()
248+
suspend fun main() {
249+
prepareEither()
250+
}
249251
//Left(InsufficientAmountOfLettuce(5))
250252
```
251253

0 commit comments

Comments
 (0)