Hi! I am going through this: <https://next.arrow-k...
# arrow
g
Hi! I am going through this: https://next.arrow-kt.io/docs/fx/polymorphism/#arrow-fx-vs-tagless-final, and there is a mention about Direct relationship between 
suspend () -> A
 and 
Kind<F, A>
. Is there any ongoing development to replace or eliminate higher kinds? If yes, can someone please help me by pointing to a doc or with a simple example. Thanks!
j
suspend () -> A ~ IO<A>
is more accurate. For
F
this is more accurate:
suspend Ctx.() -> A ~ Kind<F, A> with Ctx having restrict suspension
. The below is a bit of a rant on what kinds enable us and how an attempt at modeling this with scopes may look. Arrow's kind encoding is everywhere because it is a way of pushing constraints onto
F
but without lang support this will always be sub-par. In theory every type has a kind and a higher kind is simply a function which given a type produces a type.
IO<A> = Kind<ForIO, A> = (Type) -> IO<Type> with the first argument being A
. This is very useful for typeclasses because it allows defining behavior for higher kinds (can in this case also be called partially applied types). This leads to bad inference, complex types and the need for
.fix()
at callsite for a user in kotlin because the compiler has no native support for that. All these issues can be fixed with compiler support either by kotlin itself or a plugin with arrow-meta. But I think it is preferable to not require a compiler plugin for a lib (even if it may significantly better) so the goal is to search for a better encoding of the behavior that kinds allow us, while keeping kinds around optionally and likely outside of core. This means finding a better encoding to provide constraints for a function and that may lead us to receiver scopes which already allow somewhat dynamic constrains in functions. Mixed with suspension we can emulate the effects of many datatypes like either/lists/nullable types etc. But this runs into a problem: dynamically composing a scope. Suppose you define
fun E.f(): A where E: Error<E, *>, IO<*>
for a function that performs IO and also throws typed errors of type E. Now you have two handlers:
runError(f: Error<E, *>.() -> A): Either<E, A>
and
suspend fun runIO(f: IO<*>.() -> A): A
. You cannot call
f
if you do
runError { runIO { f() } }
. So you would need a custom
runErrorAndIO
which provides a combined scope. This sucks and is what currently keeps me from defining those constraints purely over suspend. There is also no automatic way, that I am aware of, of defining
runErrorAndIO
and I think there is also no way to define
f
such that it takes multiple scopes. Btw if you were to copy the content of
f
into
runError { runIO { copy here } }
then it would compile because kotlin will infer the receiver correctly from a nested scopes. But kotlin does not allows us to make this explicit in a type.
👌 1
To clarify why
suspend
alone is not enough for `F`:
suspend
on its own is unconstrained like
IO<A>
meaning you may do anything.
F<A>
means you can't do a thing because you have no knowledge of
F
. That is roughly the same to
suspend EmptyCtx.() -> A with EmptyCtx being restrict suspension
and
interface EmptyCtx {}
in this function you cannot call any other suspend functions (effects).
Kind<F, A>
with
MonadError<F>
in scope is equal to
suspend Error<E>.() -> A
with
interface Error<E> { suspend fun raise, suspend fun catch }
. This is the direct relation between suspend and F
👌 1
g
Grt! thanks for a detailed explanation @Jannis 🙂