I just stumbled upon <https://youtrack.jetbrains.c...
# coroutines
e
I just stumbled upon https://youtrack.jetbrains.com/issue/KT-79957 I'm ignorant about what a multi-shot continuation is. Anyone that can enlighten my dumb brain about what it is and what's a real-world use case for it?
k
A continuation is effectively the state of the call stack at some point. You can store it away to use the current thread for executing something else and then later invoke the continuation to restore the previous state and continue running from that point. Now, a multishot continuation (or generally, just continuation) is something that you can invoke multiple times: you can jump back to the state of the call stack multiple times. That means that you might call a function just once but end up returning from it several times. (This can get pretty confusing with side effects and resources, e.g. Scheme has dynamic-wind that allows you to not only run code when unwind stack (like finally) but also when moving back into the opposite direction.) As a usage example, consider a pair of functions like
arbitrary
and
fail
.
arbitrary
will take several values and return one of them, but stores the continuation and the other values so that if the calculation later fails, then
fail
can then return the next value. You can then write non-deterministic code that looks straight-line:
Copy code
val a = arbitrary(1, 2, 3, 4)
val b = arbitrary(5, 6, 3, 8)

if (a + b != 8)
    fail()

println("$a + $b = 8")
Going back to the roots with Scheme's call/cc is probably a good place to start and SICP's Chapter 4 is also great. Now in general, I think that unrestricted continuations are a bit too powerful. When you have those in your language, all bets are of. But scoped properly (e.g. Haskell's continuation monad), they can be quite useful.
y
See also my library that uses reflection to clone continuations. I have a ton of examples in the tests, especially the
effekt
ones
Also https://github.com/BenWoodworth/Parameterize is a great example, and I have a PoC using Kontinuity to implement it correctly
k
What are the high level features that can be implemented with these?
y
List comprehensions. Probabilistic DSLs. Test parameterization. The search term to look for is Effects & Handlers
k
Oh gosh I hope Kotlin doesn't get list comprehensions. Coincidentally I've been doing some python recently and they feel like a strictly less readable version of map/filter
y
No that's the thing. It's completely clear list comprehensions without restricted syntax. Think something like this (this code can literally compile with this feature, and similarly with reflection in Kontinuity right now):
Copy code
listComp {
  val foo = fooList.bind()
  ensure(foo % 2 == 0)
  foo + barList.bind()
}
//equivalent to
fooList.filter { it % 2 == 0 }.flatMap { foo -> barList.map { foo + it } }
See how the first one is very readable? And it's normal Kotlin code. You can throw in a
repeat
in there, or use property delegates, or any other Kotlin magic
e
Thanks everyone for the examples. I feel like this kind of features will be difficult to use in real world scenarios, unless you're in a very specialized environment. Honestly, I prefer the
flatMap
approach here, as it doesn't hide what's going on. The list comprehension example would probably confuse 99% of the devs I know, me included.
z
That first one is completely unreadable to me but it is pretty and symmetric at least 😂
e
pretty and symmetric at least
Ah found another dev that likes aligning statements for the better visuals 😂 Gotta agree. But really, the sequential filter + flatMap makes much more sense to me during the first visual scan of the code.
y
2 main motivator here are Probabilistic and Prolog-esque DSLs. Here's an example of a probabilistic DSL I've implemented. This would've normally needed a million
flatMap
calls, obscuring the intent quite a lot. This reads like what a mathematician would write, just transcribed into code (and the DSL runner is able to run "inference" on this code, determining its exact probability or even running it with iterative deepening, which can be faster for some very complicated programs):
Copy code
val t1exact = exactReify {
  val cloudy = flip(0.5)
  val rain = flip(if (cloudy) 0.8 else 0.2)
  val sprinkler = flip(if (cloudy) 0.1 else 0.5)
  val wetRoof = flip(0.7) && rain
  val wetGrass = (flip(0.9) && rain) || (flip(0.9) && sprinkler)
  ensure(wetGrass)
  rain
}
t1exact shouldContainExactlyInAnyOrder listOf(
  Probable(0.4581, Value.Leaf(true)),
  Probable(0.188999999999999974, Value.Leaf(false))
)
The code doesn't have to rerun the whole block for every single flip, just the section of code after the
flip
gets ran twice. This is important if you have other side effects in there, and it also speeds up the inference if you have expensive calculations. The `flip`s are unintuitive at first glance because we're not used to such Kotlin code. You get very quickly used to it though. The mental model is just that
flip
can return multiple times, and
exactReify
knows how to deal with that. There's also big advantages here when compared to `flatMap`s because you can easily compose different effects together (
ensure
actually comes from Arrow's
SingletonRaise
, and
exactReify
provides it too) and it works intuitively as you'd expect. This means you can run inference about inference, or "nested inference". Doing that with a normal monadic approach is hard, and would result in
flatMap
hell and weird types, but multishot continuations make it so that the code is in direct style (no flatmaps), composes easily, and runs pretty fast. Here's another example, this time with a Prolog-style DSL generating odd primes and their squares:
Copy code
bagOfN(10) { // generates only 10 results
  val n = odds() // chooses any odd number. In reality, it returns them in order, so 1, 3, 5, etc
  println(n)
  ensure(n > 1) // 1 is not prime
  val hasDivisor = succeeds { // this means, if this block succeeds in finding a divisor, hasDivisor is true
    val d = iota(n - 1) // returns a number between 1 and n-1
    ensure(d > 1 && n % d == 0) // succeed only if d is a divisor of n 
  }
  ensure(!hasDivisor)
  if (flip()) n else n * n // either a prime or its square
} shouldBe listOf(3, 9, 5, 25, 7, 49, 11, 121, 13, 169)
Only the odd numbers up to 13 will be printed, showing that this isn't simply rerunning the whole block. This also means that the
flip
call doesn't result in recomputing
hasDivisor
unnecessarily, which would be awful for performance