-
-
Save elizarov/5bbbe5a3b88985ae577d8ec3706e85ef to your computer and use it in GitHub Desktop.
import kotlin.coroutines.* | |
import kotlin.coroutines.intrinsics.* | |
/** | |
* Implementation for Delimited Continuations `shift`/`reset` primitives via Kotlin Coroutines. | |
* See [https://en.wikipedia.org/wiki/Delimited_continuation]. | |
* | |
* The following LISP code: | |
* | |
* ``` | |
* (* 2 (reset (+ 1 (shift k (k 5))))) | |
* ``` | |
* | |
* translates to: | |
* | |
* ``` | |
* 2 * reset<Int> { | |
* 1 + shift<Int> { k -> k(5) } | |
* } | |
* ``` | |
*/ | |
fun <T> reset(body: suspend DelimitedScope<T>.() -> T): T = | |
DelimitedScopeImpl<T>().also { impl -> | |
body.startCoroutine(impl, impl) | |
}.runReset() | |
interface DelimitedContinuation<T, R> | |
@RestrictsSuspension | |
abstract class DelimitedScope<T> { | |
abstract suspend fun <R> shift(block: suspend DelimitedScope<T>.(DelimitedContinuation<T, R>) -> T): R | |
abstract suspend operator fun <R> DelimitedContinuation<T, R>.invoke(value: R): T | |
} | |
private typealias ShiftedFun<T> = (DelimitedScope<T>, DelimitedContinuation<T, Any?>, Continuation<T>) -> Any? | |
@Suppress("UNCHECKED_CAST") | |
private class DelimitedScopeImpl<T> : DelimitedScope<T>(), Continuation<T>, DelimitedContinuation<T, Any?> { | |
private var shifted: ShiftedFun<T>? = null | |
private var shiftCont: Continuation<Any?>? = null | |
private var invokeCont: Continuation<T>? = null | |
private var invokeValue: Any? = null | |
private var result: Result<T>? = null | |
override val context: CoroutineContext | |
get() = EmptyCoroutineContext | |
override fun resumeWith(result: Result<T>) { | |
this.result = result | |
} | |
override suspend fun <R> shift(block: suspend DelimitedScope<T>.(DelimitedContinuation<T, R>) -> T): R = | |
suspendCoroutineUninterceptedOrReturn { | |
this.shifted = block as ShiftedFun<T> | |
this.shiftCont = it as Continuation<Any?> | |
COROUTINE_SUSPENDED | |
} | |
override suspend fun <R> DelimitedContinuation<T, R>.invoke(value: R): T = | |
suspendCoroutineUninterceptedOrReturn sc@{ | |
check(invokeCont == null) | |
invokeCont = it | |
invokeValue = value | |
COROUTINE_SUSPENDED | |
} | |
fun runReset(): T { | |
// This is the stack of continuation in the `shift { ... }` after call to delimited continuation | |
var currentCont: Continuation<T> = this | |
// Trampoline loop to avoid call stack usage | |
loop@while (true) { | |
// Call shift { ... } body or break if there are no more shift calls | |
val shifted = takeShifted() ?: break | |
// If shift does not call any continuation, then its value becomes the result -- break out of the loop | |
try { | |
val value = shifted.invoke(this, this, currentCont) | |
if (value !== COROUTINE_SUSPENDED) { | |
result = Result.success(value as T) | |
break | |
} | |
} catch (e: Throwable) { | |
result = Result.failure(e) | |
break | |
} | |
// Shift has suspended - check if shift { ... } body had invoked continuation | |
currentCont = takeInvokeCont() ?: continue@loop | |
val shiftCont = takeShiftCont() | |
?: error("Delimited continuation is single-shot and cannot be invoked twice") | |
shiftCont.resume(invokeValue) | |
} | |
// Propagate the result to all pending continuations in shift { ... } bodies | |
currentCont.resumeWith(result!!) | |
// Return the final result | |
return result!!.getOrThrow() | |
} | |
private fun takeShifted() = shifted?.also { shifted = null } | |
private fun takeShiftCont() = shiftCont?.also { shiftCont = null } | |
private fun takeInvokeCont() = invokeCont?.also { invokeCont = null } | |
} |
import org.junit.* | |
import kotlin.test.* | |
class DelimitedTest { | |
@Test | |
fun testNoShit() { | |
val x = reset<Int> { 42 } | |
assertEquals(42, x) | |
} | |
@Test | |
fun testShiftOnly() { | |
val x = reset<Int> { | |
shift<Int> { k -> k(42) } | |
} | |
assertEquals(42, x) | |
} | |
@Test | |
fun testShiftRight() { | |
val x = reset<Int> { | |
40 + shift<Int> { k -> k(2) } | |
} | |
assertEquals(42, x) | |
} | |
@Test | |
fun testShiftLeft() { | |
val x = reset<Int> { | |
shift<Int> { k -> k(40) } + 2 | |
} | |
assertEquals(42, x) | |
} | |
@Test | |
fun testShiftBoth() { | |
val x = reset<Int> { | |
shift<Int> { k -> k(40) } + | |
shift<Int> { k -> k(2) } | |
} | |
assertEquals(42, x) | |
} | |
@Test | |
fun testShiftToString() { | |
val x = reset<String> { | |
shift<Int> { k -> k(42) }.toString() | |
} | |
assertEquals("42", x) | |
} | |
@Test | |
fun testShiftWithoutContinuationInvoke() { | |
val x = reset<Int> { | |
shift<String> { | |
42 // does not call continuation, just override result | |
} | |
0 // this is not called | |
} | |
assertEquals(42, x) | |
} | |
// From: https://en.wikipedia.org/wiki/Delimited_continuation | |
// (* 2 (reset (+ 1 (shift k (k 5))))) | |
// k := (+ 1 []) | |
@Test | |
fun testWikiSample() { | |
val x = 2 * reset<Int> { | |
1 + shift<Int> { k -> k(5) } | |
} | |
assertEquals(12, x) | |
} | |
// It must be extension on DelimitedScope<Int> to be able to shift | |
private suspend fun DelimitedScope<Int>.shiftFun(x: Int): Int = | |
shift<Int> { k -> k(x) } * 2 | |
@Test | |
fun testShiftFromFunction() { | |
val x = reset<Int> { | |
2 + shiftFun(20) | |
} | |
assertEquals(42, x) | |
} | |
@Test | |
// Ensure there's no stack overflow because of many "shift" calls | |
fun testManyShifts() { | |
val res = reset<String> { | |
for (x in 0..10000) { | |
shift<Int> { k -> | |
k(x) | |
} | |
} | |
"OK" | |
} | |
assertEquals("OK", res) | |
} | |
@Test | |
// See https://gist.github.com/elizarov/5bbbe5a3b88985ae577d8ec3706e85ef#gistcomment-3304535 | |
fun testShiftRemainderCalled() { | |
val log = ArrayList<String>() | |
val x = reset<Int> { | |
val y = shift<Int> { k -> | |
log += "before 1" | |
val r = k(1) | |
log += "after 1" | |
r | |
} | |
log += y.toString() | |
val z = shift<Int> { k -> | |
log += "before 2" | |
val r = k(2) | |
log += "after 2" | |
r | |
} | |
log += z.toString() | |
y + z | |
} | |
assertEquals(3, x) | |
assertEquals(listOf( | |
"before 1", | |
"1", | |
"before 2", | |
"2", | |
"after 2", | |
"after 1" | |
), log) | |
} | |
} |
@steshaw I believe that code ended up getting removed through the many evolution steps of Arrow. IMO, some of it was "too hacky" which resulted in it getting removed.
A spiritual successor to that code (although it shares almost none of its implementation) is my so-far-successful attempt at building multiprompt delimited continuations in Kotlin. It theoretically has a single-shot core that uses no "magic" (i.e reflection or platform-specific code) but I haven't extracted that yet. It uses a tiny API surface to shallow clone continuations for multishot cases (that surface just being val Continuation<*>.completion: Continuation<*>
and fun <T> Continuation<T>.copy(completion: Continuation<*>): Continuation<T>
, but the former isn't even quite "necessary" if we change some of the behaviour of the latter, but it's useful performance-wise). It currently works only for JVM and JS because I can't figure out how to shallow-clone on other platforms yet.
You can find some of the original Arrow test code in my repo's tests.
I'm pretty convinced that my implementation is nearly-correct. It passes a lot of non-obvious tests and seems to demonstrate the multiprompt behaviour, and hence any bugs are likely to be squashable easily. The implementation is based on @b-studios 's java-effekt
but with some Kotlin-specific nuances, and a bunch of optimizations for single-shot resumptions. In fact, the repo has further examples of adding a thin layer on multiprompt delimited continuations to turn them into "Lexical Effect Handlers", which is the idea that java-effekt presents. Further, the language Effekt evolved directly from that, and pretty much every one of its examples can thus be translated directly to Kotlin (check out the effekt/casestudies tests folder for that)
I'd be glad to hear your feedback if you choose to play around with it :)
@elizarov I figured
testNoShit
is named incorrectly