https://kotlinlang.org logo
#arrow
Title
# arrow
n

Nathan Bedell

03/02/2024, 5:44 PM
Hey all, I'm curious, is it possible in Kotlin to build a monad comprehension syntax for the monad
Free (Map a)
? I want to say my gut intuition is "no" as the
Map a
branching would probably require multi-shot continuations, but I thought I'd ask anyway.
s

simon.vergauwen

03/02/2024, 6:19 PM
I'm curious what your use-case is, or if it's curiosity 😉 Also, maybe I misunderstood your question.. 😅
Map
typically disappears in Arrow DSL, and
flatMap
typically as well.
flatMap
has couple of uses, but multi-shot is impossible so all multi-shot
flatMap
are out of the way. Although you can still do a lot of cool things with them in Kotlin, but design becomes very complex. Sweet spot seems relying on
List
,
Sequence
or
Flow
depending on what kind of multi-shot effect you need, and mix what we don't have an official label for but scope/dsl or effect. Let's take
Raise
, which is like
MonadError
, it's a
Monad
and a
Functor
but we dropped that hierarchy `map`/`flatMap`. So all that remains is raising an error, and recovering from an error. So
raise
, and
recover
, everything else is derived from that. Similarly with
ResourceScope
, all you need is
install
to register a "acquisition" action, and a "finalizer" action. Everything else is derived from there, like registering cancellation, or error callbacks, etc. So it feels a bit similar to designing in Free to me. In Free you typically also think about the operations, and you do the same here but instead of thinking of them in an ADT. You need to think of them in terms of operations.
Copy code
sealed class Resource<A>
data class Install<A>(
  val acquire: suspend () -> A,
  val release: suspend (ExitCase, A) -> Unit
): Resource<A>
vs
Copy code
interface ResourceScope {
  fun <A> install(
    acquire: suspend () -> A,
    release: suspend (ExitCase, A) -> Unit
  ): A
}
If you look at the signatures, they're identical. Next you typically write the interpeter, loop, eval, ... for your Free ADT. (Regardless if you're using a Free abstraction, or not). In the case of the DSLs, you are stuck writing a
class
implementation. So you often have to resort to
AtomicRef
instead of
tailrec
since you loose some of the structure. The reason for doing it this way is because this is compatible with the entire Kotlin eco-system. It'll play incredible nice with context parameters, and it's easy to support coroutine cancellation and everything. While this doesn't sounds great initially I actually found that my implementation are often much simpler, and easy to maintain. Might depend on the use-case of course, but you can often create smaller DLSs and compose them into bigger functionality as well. This composes, while monads or free still require transformers, etc.
n

Nathan Bedell

03/02/2024, 6:24 PM
@simon.vergauwenYeah, so for clarity, essentially what I want a comprehension syntax for is something like:
Copy code
sealed interface FreeMap<A, B> {
    data class Pure(val result: B) : FreeMap<Any?, B>
    data class Continue(val next: Map<A, FreeMap<A, B>>) : FreeMap<A,B>
}
The motivation for this is essentially to get a monad comprehension syntax for a "coroutine" -- not in the sense of Kotlin coroutines, but as in the Haskell package monad-coroutine (https://hackage.haskell.org/package/monad-coroutine-0.9.2/docs/Control-Monad-Coroutine.html) In particular, I've been exploring this kind of data structure as a sort of dual to the idea of "coroutines as spaces". The idea is something of type
FreeMap<Action, Result>
is a function which requires user input (in the form of an
Action
) at "suspension points", but also at each suspension point has a list of allowable
Actions
(hence the
Map<Action, FreeMap<Action, Result>>
). For instance, imagine a UI Workflow like a sequence of dialogs that at each stage the user has a number of different `Action`s for how to proceed -- and we want to view this as a composable process that eventually returns a
Result
. That's the sort of thing I'm trying to represent here. And the advantage of being able to explicitly represent the "set of allowed actions" at each stage is that it makes it really easy to property test it (e.x. test a bunch of possible paths of user actions, assert some invariants / temporal properties / etc...) 🙂
3 Views