Edoardo Luppi
08/07/2025, 12:51 PMkomu
08/07/2025, 1:38 PMarbitrary
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:
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.Youssef Shoaib [MOD]
08/07/2025, 2:25 PMeffekt
onesYoussef Shoaib [MOD]
08/07/2025, 2:28 PMkevin.cianfarini
08/07/2025, 4:58 PMYoussef Shoaib [MOD]
08/07/2025, 5:00 PMkevin.cianfarini
08/07/2025, 5:08 PMYoussef Shoaib [MOD]
08/07/2025, 5:13 PMlistComp {
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 magicEdoardo Luppi
08/07/2025, 5:18 PMflatMap
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.Zach Klippenstein (he/him) [MOD]
08/08/2025, 2:51 AMEdoardo Luppi
08/08/2025, 7:44 AMpretty and symmetric at leastAh 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.
Youssef Shoaib [MOD]
08/09/2025, 2:55 PMflatMap
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):
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:
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