Hi, I wish to short-circuit `fx.async{}` block bel...
# arrow
g
Hi, I wish to short-circuit
fx.async{}
block below:
Copy code
fun <F> Async<F>.something(): Kind<F, Either<String, String>> = fx.async {
    val left = "1-left".left().just()
    // I wish to short-circuit here if either above is left
    "2-right".right()
}
This is what I could come up with:
Copy code
fun <F> Async<F>.something2(): Kind<F, Either<String, String>> = fx.async {
    Either.fx {
        !"1-left".left()
        "2-right".right().bind()
    }
}
Is there a better way than to use nested blocks?
j
That is kind of the point of mtl: In fact the datatype
EitherT<L, F, A>
is a wrapper around
Kind<F, Either<L, A>>
which has a monad instance that flatMaps through the nested either. I do not recommend using it directly in code, what should be done instead is:
fun <F, E, A> something(AS: Async<F>, ME: MonadError<F, E>): Kind<F, A>
and then later use
EitherT.async
and
EitherT.monadError
to fill in instances. I know I said "I do not recommend using it directly in code" just above, but currently this won't be possible because `EitherT`'s
MonadError
instance is wrong! (Just noticed that). So if you use
fun something(AS: Async<F>): EitherT<L, F, A> = EitherT.async(AS).fx {}
(or similar, don't quite remember the exact syntax) you should get a binding where you can do
EitherT.left(error).bind()
and it will short circuit. I think with 0.11 you can also do the same thing with bifunctor
IO
. So you can call
something(IO.async(), IO.monadError())
. But if you don't use
IO
mtl is your best bet. cc @simon.vergauwen is my assumption that
MonadError
will use
E
and not
Throwable
correct? o.O This is quite the ugly pattern right now, but just imagine this pattern with actual typeclass resolution from arrow meta. That is when this pattern becomes really powerful and easy to use. For right now and maybe further reading: There is no good way other than what you are doing. Monads do not compose nicely and there are a lot of different attempts to solve these problems: • Bifunctor IO fuses two commonly used monads to avoid the need for composing monads (0.11
IO
type in arrow) • mtl composes monads through transformers (lots of little drawbacks everywhere, but better than manually composing them. This pattern is ugly in kotlin without arrow-meta, which is not yet done :/) • algebraic effect systems (just look it up if you are interested. The basic thing is possible using
Free
in arrow, but not without quite a lot overhead in both boilerplate and performance. With meta it might be possible to get a fast effect system...). This pattern is imo the best solution there is, but that is a long time away in kotlin/arrow, if at all possible. Sorry for the long answer, this also touches a lot of complex topics, so if you have questions don't hesitate to ask!
Oh one additional note:
EitherT
has it's generic argument order changed in the latest snapshot. I used the new order
EitherT<L, F, A>
here. If you use an older version
0.10.4
or prior just substitute that with
EitherT<F, L, A>
s
cc @simon.vergauwen is my assumption that
MonadError
will use
E
and not
Throwable
correct? o.O
You can define lawful instances for both.
j
Which means there needs to be a choice. Going
E
as the default instance and using
MonadThrow
for
Throwable
sounds most sensible to me.
EitherT
also needs to make that choice for
MonadError
,
MonadThrow
and
Bracket
and I am not quite sure whats best.
s
MonadThrow
for
Throwable
sounds most sensible
Yes aboslutely! Didn’t think about
MonadThrow
Can we take a similar approach for
EitherT
?
j
Should be doable by lifting through and using the
MonadThrow
of
F
.
Bracket
is not so easy of a choice though
s
Yes, I’d need to check
Bracket
what we can do in the impl.
We could introduce a couple interessing additonal typeclasses.
BracketThrow<F, E>
with
ExitCaseThrow<E>
… need to play with types 😄
j
Thing is: If we go
IOBracket<E> : Bracket<IOPartialOf<E>, E>
then there is no way for a user to define bracket for
Throwable
(without going to internals or local functions for
IO
). These are the hard parts of bifunctor
IO
and in general
EitherT
error handling. If we choose the
Throwable
instance it might be weird for those that extensively use
E
. If we choose
E
it will be very hard to handle unexpected errors thrown from impure code of which there is plenty out there in the jvm ecosystem. Exposing both seems best, but not through additional typeclasses, because that is an
IO
specific problem.
EitherT
doesn't have this problem because you can always go a level lower and use
IO
if you need it, with bifunctor
IO
and the wrong instance that choice is taken away. The problem is that with typeclass resolution there is likely a need to constrain it to one instance only, so we might need to go for additional typeclasses just to have our
IO
work in a polymorphic setting. We also need to expose multiple versions of bracket: One for
E
and one for
Throwable
. This is a bit messy 😅
s
Arrow Meta Fast mtl? with a fused effects inspired API? 😬
j
Being able to do that would be amazing, especially because mtl types can easily become carriers for effects in algebraic effect systems. We'd "just" need a typelevel way of composing them correctly. Which is really hard. Looking at haskell might not even work this time around because they tailor their implementations to ghc to be fast, which is unlikely to apply to kotlin
😮 1