Gopal S Akshintala
08/02/2020, 1:06 PMsuspend () -> 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!Jannis
08/02/2020, 2:17 PMsuspend () -> 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.Jannis
08/02/2020, 2:21 PMsuspend
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 FGopal S Akshintala
08/02/2020, 5:02 PM