Has there been internal discussions about the idea...
# language-evolution
y
Has there been internal discussions about the idea of introducing multi-prompt delimited continuations? Any keeps or discussions I can read? From reading the literature, it seems that the standard CPS is sufficient for implementing all the strange flavours of control operators, and in fact that the control operators can be implemented as a simple 4-method library (provided that the continuations are multishot). Since Kotlin's continuations are specialized to be single-shot, a compiler change would be needed to make them shallow cloneable, but other than that, it seems that this code could be easily shipped in the stdlib without increasing the binary size massively or anything (perhaps in the intrinsics package because those operators are complicated for the average user)
s
What is the use case for multi-shot Continuations vs collecting a (cold) Flow?
y
It allows for direct-style syntax so that one can do e.g. flow comprehensions. Imagine you could do something like:
Copy code
flowComprehension {
  val value1 = flow1.collectHere()
  val value2 = flow2.collectHere()
  ...
  value1 + value2
} // Returns a Flow<Int> as a result
This syntax wouldn't be only for flows though, it would work for any datatype (technically it's for Monads but that's an often-confusing term). Similar code can be written for a list comprehension, a sequence comprehension, etc. One can also use different types with it:
Copy code
flowComprehension {
  listComprehension {
    // One can collect flows or lists here freely as they wish. Probably. It'd depend on a nice-enough implementation
  }
}
Look, it's similar to why have suspend functions vs just using callbacks. Any code written with suspend can also be written with callbacks. Similarly here, any code with multishot continuations can just be written with callbacks. The advantage of writing it in direct style (other than aesthetics and ease of understanding) is also that the compiler can perform certain optimizations automatically.
👍 3
b
I'm just thinking out loud > The advantage of writing it in direct style (other than aesthetics and ease of understanding) The difference between "suspend functions vs just using callbacks" (1) and "multi-shot continuations vs just using suspend callbacks" (2) is that in (1), it's desireable to transform this kind of callback hell to flatten "one level" sequential code, because specifically this kind of callback hell represents a sequential execution. In (2), it's unclear to me whether it's desirable to express non-sequential computation in a sequential manner. (2) can end up being a good puzzler Multi-shot continuations in (2) can simplify writing the code (because programmers don't need to remember all these weird operator names). But will it simplify reading the code is the question It's especially hard to reason about multi-shot continuations produced by suspend functions that are invoked inside a loop or a
try-catch-finally
. Contrary, in (1), suspend functions combined with `for`/`try-catch` was a selling point (again, because programmers don't need to remember names of the operators)
y
I agree that yes try-catch-finally semantics is hard, but that always will require new semantics anyways (because multi-shot can be re-entered multiple times) and thus it requires new design. I think something like dynamic-wind could be an interesting prototype for that idea, I'm experimenting around with various designs rn. So yes resources and closeables are harder to reason about with multi-shot, but that's an inherit design problem with mutli-shotness, and solutions to manage such resources, I think, would be in fact easier to express in direct-style code than with callback hell. Yes intuitions will have to change slightly, but there's techniques that can help (Arrow resource scopes come to mind for instance) The following morphed into a few semi-comprehensible thoughts that I need to clean up with concrete examples: However, if one isn't dealing with resource acquiring and closing, there's very big gains in expressivity. It's hard to give examples that don't look like toys (I'll provide some when I'm not on mobile) but the main area of literature that showcases this is effect handlers. Effect handlers need multi-shot continuations (and some notion of dynamic binding i.e. coroutine-local state, which coroutineContext fits perfectly!) and they allow programming to interfaces that can have control effects. That allows to hide away the complex code that needs to deal with Continuations directly into libraries, and provide a clean DSL on top to represent e.g. Parsers, non-local control flow, non-determinism, and probabilistic computations. It also allows combining these effects together really really easily (and if you know monad transformers, it turns out that these effect handlers are more flexible than transformers, in fact they can represent things that transformers can't, but they can do everything that a transformer can). One final point is that this meshes really nicely with context receivers and DSLs to provide a notion of "capability-passing" i.e. that functions need to receive an object implementing some effect interface to be able to do that effect. There's a very neat OOP formulation of effect handlers provided in the paper "Effect handlers for the masses" which implements them in Java and Scala and shows that they work nicely with imperative programming and OOP. That paper resulted in a library called java-effekt, and I've recently experimented and successfully implemented a lot of the ideas and tests from that library. You can see a bunch of motivating examples here if you'd like to explore it Again sorry this became quite ramble-y; I need to collect my thoughts further about this and provide some stronger arguments than just "it looks neat!" 😁
s
From your original example, I think this is already somewhat possible, slightly rewritten, for event flows (won't work for state flows, and it starts a new pair of collections each time around) 😀
Copy code
flow { 
  val value1 = flow1.first() 
  val value2 = flow2.first()
  emit(value1 + value2)
} // Returns a Flow<Int> as a result
y
That's only for the first item though, no? I want behaviour similar to
flatMapConcat
1
s
Oops, I forgot the loop around it