Skip to content

Commit 54c4e29

Browse files
authored
Provide nullable continuation based on DelimitedScope (#251)
* Provide nullable { } implementation based on DelimitedScope * provide singleton object with reference check for nested nullable value. * Provide extra test cases for nullable.kt * remove check problems with detekt * provide TODO for future fun interface * reduce library visibility for API users. * nextShift values doesn't need to be nested nullable value. * Fix warning for parameter renaming for overridden method. * Eliminate the shift call on non-null values * move NullableBindSyntax.kt into private inner class for nullable.kt * fix argument ending * trigger GitHub actions * trigger GitHub actions
1 parent 90cc8ac commit 54c4e29

File tree

4 files changed

+112
-12
lines changed

4 files changed

+112
-12
lines changed

arrow-libs/core/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimContScope.kt

+16-12
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class DelimContScope<R>(val f: suspend DelimitedScope<R>.() -> R) : DelimitedSco
2525
/**
2626
* Variable used for polling the result after suspension happened.
2727
*/
28-
private val resultVar = atomic<R?>(null)
28+
private val resultVar = atomic<Any?>(EMPTY_VALUE)
2929

3030
/**
3131
* Variable for the next shift block to (partially) run, if it is empty that usually means we are done.
@@ -61,20 +61,20 @@ class DelimContScope<R>(val f: suspend DelimitedScope<R>.() -> R) : DelimitedSco
6161
}
6262

6363
/**
64-
* Captures the continuation and set [func] with the continuation to be executed next by the runloop.
64+
* Captures the continuation and set [f] with the continuation to be executed next by the runloop.
6565
*/
66-
override suspend fun <A> shift(func: suspend DelimitedScope<R>.(DelimitedContinuation<A, R>) -> R): A =
66+
override suspend fun <A> shift(f: suspend DelimitedScope<R>.(DelimitedContinuation<A, R>) -> R): A =
6767
suspendCoroutine { continueMain ->
6868
val delCont = SingleShotCont(continueMain, shiftFnContinuations)
69-
assert(nextShift.compareAndSet(null, suspend { this.func(delCont) }))
69+
assert(nextShift.compareAndSet(null, suspend { this.f(delCont) }))
7070
}
7171

7272
/**
7373
* Same as [shift] except we never resume execution because we only continue in [c].
7474
*/
75-
override suspend fun <A, B> shiftCPS(func: suspend (DelimitedContinuation<A, B>) -> R, c: suspend DelimitedScope<B>.(A) -> B): Nothing =
75+
override suspend fun <A, B> shiftCPS(f: suspend (DelimitedContinuation<A, B>) -> R, c: suspend DelimitedScope<B>.(A) -> B): Nothing =
7676
suspendCoroutine {
77-
assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) }))
77+
assert(nextShift.compareAndSet(null, suspend { f(CPSCont(c)) }))
7878
}
7979

8080
/**
@@ -83,22 +83,23 @@ class DelimContScope<R>(val f: suspend DelimitedScope<R>.() -> R) : DelimitedSco
8383
override suspend fun <A> reset(f: suspend DelimitedScope<A>.() -> A): A =
8484
DelimContScope(f).invoke()
8585

86+
@Suppress("UNCHECKED_CAST")
8687
fun invoke(): R {
8788
f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result ->
8889
resultVar.value = result.getOrThrow()
8990
}).let {
9091
if (it == COROUTINE_SUSPENDED) {
9192
// we have a call to shift so we must start execution the blocks there
9293
resultVar.loop { mRes ->
93-
if (mRes == null) {
94+
if (mRes === EMPTY_VALUE) {
9495
val nextShiftFn = nextShift.getAndSet(null)
9596
?: throw IllegalStateException("No further work to do but also no result!")
9697
nextShiftFn.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { result ->
9798
resultVar.value = result.getOrThrow()
98-
}).let {
99+
}).let { nextRes ->
99100
// If we suspended here we can just continue to loop because we should now have a new function to run
100101
// If we did not suspend we short-circuited and are thus done with looping
101-
if (it != COROUTINE_SUSPENDED) resultVar.value = it as R
102+
if (nextRes != COROUTINE_SUSPENDED) resultVar.value = nextRes as R
102103
}
103104
// Break out of the infinite loop if we have a result
104105
} else return@let
@@ -107,15 +108,18 @@ class DelimContScope<R>(val f: suspend DelimitedScope<R>.() -> R) : DelimitedSco
107108
// we can return directly if we never suspended/called shift
108109
else return@invoke it as R
109110
}
110-
assert(resultVar.value != null)
111+
assert(resultVar.value !== EMPTY_VALUE)
111112
// We need to finish the partially evaluated shift blocks by passing them our result.
112113
// This will update the result via the continuations that now finish up
113-
for (c in shiftFnContinuations.asReversed()) c.resume(resultVar.value!!)
114+
for (c in shiftFnContinuations.asReversed()) c.resume(resultVar.value as R)
114115
// Return the final result
115-
return resultVar.value!!
116+
return resultVar.value as R
116117
}
117118

118119
companion object {
119120
fun <R> reset(f: suspend DelimitedScope<R>.() -> R): R = DelimContScope(f).invoke()
121+
122+
@Suppress("ClassName")
123+
private object EMPTY_VALUE
120124
}
121125
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package arrow.core.computations
2+
3+
import arrow.continuations.generic.DelimContScope
4+
import arrow.continuations.generic.DelimitedScope
5+
import arrow.core.computations.suspended.BindSyntax
6+
7+
@Suppress("ClassName")
8+
object nullable {
9+
operator fun <A> invoke(func: suspend BindSyntax.() -> A?): A? =
10+
DelimContScope.reset { NullableBindSyntax(this).func() }
11+
12+
private class NullableBindSyntax<R>(
13+
scope: DelimitedScope<R?>
14+
) : BindSyntax, DelimitedScope<R?> by scope {
15+
override suspend fun <A> A?.invoke(): A =
16+
this ?: shift { null }
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package arrow.core.computations.suspended
2+
3+
/**
4+
* Running A? in the context of [nullable]
5+
*
6+
* ```
7+
* nullable {
8+
* val one = 1.invoke() // using invoke
9+
* val bigger = (one.takeIf{ it > 1 }).invoke() // using invoke on expression
10+
* bigger
11+
* }
12+
* ```
13+
*/
14+
// TODO: this will become interface fun when they support suspend in the next release
15+
interface BindSyntax {
16+
suspend operator fun <A> A?.invoke(): A
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package arrow.core.computations
2+
3+
import arrow.core.test.UnitSpec
4+
import io.kotlintest.shouldBe
5+
6+
class NullableTest : UnitSpec() {
7+
8+
init {
9+
"simple case" {
10+
nullable {
11+
"s".length().invoke()
12+
} shouldBe 1
13+
}
14+
"multiple types" {
15+
nullable {
16+
val number = "s".length()
17+
val string = number.toString()()
18+
string
19+
} shouldBe "1"
20+
}
21+
"short circuit" {
22+
nullable {
23+
val number: Int = "s".length()
24+
(number.takeIf { it > 1 }?.toString())()
25+
throw IllegalStateException("This should not be executed")
26+
} shouldBe null
27+
}
28+
"when expression" {
29+
nullable {
30+
val number = "s".length()
31+
val string = when (number) {
32+
1 -> number.toString()
33+
else -> null
34+
}.invoke()
35+
string
36+
} shouldBe "1"
37+
}
38+
"if expression" {
39+
nullable {
40+
val number = "s".length()
41+
val string = if (number == 1) {
42+
number.toString()
43+
} else {
44+
null
45+
}.invoke()
46+
string
47+
} shouldBe "1"
48+
}
49+
"if expression short circuit" {
50+
nullable {
51+
val number = "s".length()
52+
val string = if (number != 1) {
53+
number.toString()
54+
} else {
55+
null
56+
}()
57+
string
58+
} shouldBe null
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)