Nathan Bedell
03/02/2024, 5:44 PMFree (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.simon.vergauwen
03/02/2024, 6:19 PMMap
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.
sealed class Resource<A>
data class Install<A>(
val acquire: suspend () -> A,
val release: suspend (ExitCase, A) -> Unit
): Resource<A>
vs
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.Nathan Bedell
03/02/2024, 6:24 PMsealed 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>
}
Nathan Bedell
03/02/2024, 6:30 PMFreeMap<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...) 🙂