wanted to test the `IO<E, A>` API with the ...
# arrow
s
wanted to test the
IO<E, A>
API with the Functional and Reactive Domain Modeling sample project.... it looks great, way better than Monad transformers IMO https://github.com/arrow-kt/frdomain.kt/pull/11
👏 5
r
Most of those Kleislis could be replaceable with extension functions:
Copy code
fun Account.calculateInterest(): IO<AccountServiceException, Amount>
☝️ 1
And it should considerably ease the type juggling around Kleisli and partial application of IO
j
While I agree that
IO<E, A>
>
EitherT<ForIO, E, A>
using concrete types in mtl (apart from at the edge) is an anti-pattern imo. Mtl code should look like this
fun m(ME: MonadError<F, E>, MIO: MonadIO<F>): Kind<F, A>
(or similar) and then at the edge of your application (similar to running IO) you run the monad stack by filling in the instances. If
m
would be at the edge you'd do
m(EitherT.monadError(), EitherT.monadIO(IO.monadIO()))
. This is still ugly, but just picture this with arrow-meta and automatic typeclass resolution, you would not to need manually fill this in. This is still more involved than
IO<E, A>
if you just need
IO
+
MonadError<E>
, but as soon as you layer more effects (if you ever need them) it becomes much more powerful
r
Also some could be modeled with just:
Copy code
suspend fun Account.calculateInterest(): Either<AccountServiceException, Amount>
since
suspend () -> Either<E, A>
<->
IO<E, A>
s
@Jannis we also have that version in the repo, but that one still works with
IO<A>
(haven't converted it, yet)
@raulraja do you have a complete example? how would you get the account from the Repo? 🤔
r
You wouldn’t need to since the syntax would be activated anywhere where the repo is in scope
Kleisli<IOPartialOf<E>, D, A>
is the same as
D.() -> IO<E, A>
or
D.() -> suspend () -> Either<E, A>
s
got it, thanks
r
The technique is just let
R
be the environment and adhere to any capabilities you want. The have an edge of the world Module that proofs that
R
can be those capabilities by delegation https://gist.github.com/raulraja/97e2d5bf60e9d96680cf1fddcc90ee67#file-direceivers-kt-L51
same as Kleisli but without the wrapping since Kotlin supports extension functions and suspend which are also faster to dispatch than Kleisli and leaner in memory
s
might be time for a third module in the sample repo
r
xD
I think the issue is in the instance of MonadError for IO
Copy code
@extension
interface IOMonadError<E> : MonadError<IOPartialOf<E>, Throwable>, IOApplicativeError<E>, IOMonad<E>
That
Throwable
should be
E
Maybe something like:
I believe we still need to go over the effects type classes to make them aware of E since they should extend the MonadError of E and Throwable becomes implicit from Async and up
👍 1
But @simon.vergauwen may know more.
j
The issue of having two valid and useful error instances is quite annoying, going for bifunctor effects instead of the current ones is probably the cleanest solution, but yeah the problem is a balancing act because both access to throwable and E is important, especially confusing with bracket
s
What is the rationale behind having two error channels? IMHO I this case less is more, a single polymorphic error channel should be enough...
s
Exceptions need to be treated differently than normal domain errors
If you’d mix the two you end up with
Either
OR
IO
not both
So you could do
() -> Either<E, A>
or
suspend () -> A
but never
suspend () -> Either<E, A>
Which is not what you want for an effect systetm
s
Well, I'm going with the assumption that when exception occurs: it's either a domain error in which case you want it in the
E
error channel (that way you can handle and eliminate it later) or crash
j
A single polymorphic error channel is not possible on the jvm (even haskell has IOExceptions but there they can be mostly ignored because it is so rarely used), exceptions happen especially in third party code, now you could obviously ignore those and call them fatal but what exactly does that mean for methods that guarantee finalizers like bracket?
bracket is the real problem here: With just one typed error channel a lot of the guarantees you provide are gone when we ignore that exceptions do happen and thus it needs perfect discipline to never throw which means we cannot wrap third party apis in bracket without manual exception handling
s
move them in the
E
channel maybe? otherwise you have 2 possible type signature for a function that reads a string form a file:
IO<Nothing, String>
and
IO<IOException, String>
and the later is clearly superior since it can eliminate the error using
handleErrorWith
j
I think the lesser evil is just accepting exceptions happen and treat them as a secondary cause for failure in IO. The only problem is that instances are then harder to write because instances for the typed error and the throwable are both valid and useful, but having two instances is not an option. So instead a compromise would be to go with the typed error in MonadError, the throwable in MonadThrow and both in the rest of the effect hierarchy (including bracket)
s
just my $ .2
j
move them in the
E
channel maybe?
That does not work, we and neither the user can predict if something can fail and with what exception so how would you move it to E? You could say that constructing
IO<Nothing, A>
means the exceptions should be fatal, but that again is an api that will end up with users doing the wrong thing unintentionally. Imo it is more important to never fail to catch exceptions because otherwise bracket becomes unpredictable and that would be awful.
Exceptions are a pain, but treating code as if they don't exists will bite you at some point
s
That does not work, we and neither the user can predict if something can fail and with what exception so how would you move it to E?
having a different function for creating IO from third party code that creates
IO<Throwable, A>
safely
having bracket, guarantee etc.... work correctly is not negotiable... I agree with that however if 2 error channels can be avoided given those constraints, I would rather go with one
j
I don't think 2 error channels are a problem at all as long as you don't treat them as 2. Your application code is perfectly fine using only one and going back to the exception one top level. It is only the instances for polymorphic code that become more problematic
And tbh I think it is better for a library like fx to catch all exceptions regardless of constructor method than to only catch a subset of exceptions because when catching all we effectively protect the user from (inevitably) shooting themselves in the foot. Especially around bracket having both errors available means it never fails regardless of what code is thrown against it, and afaik this catch-all comes at basically no cost (cc @simon.vergauwen). The only cost of having two channels is the mental overhead when actually dealing with the Throwable one, which you only need to do when you expect exceptions to be thrown or when running code top-level
s
let me play Devil's advocate here: it also increase the API surface you can use
raiseError
and
raiseException
for creating your IO you also have 2 sets of error handling recovery operators, 2 sets of "side effect on error" operators etc....
s
That’d not be safe, and would not be a safe effect system for the JVM. Or in other words it comes with all the short comings of doing manual effects
j
Well technically you only need two
raise
and
handleWith
everything else is derived from monad error anyway. And I don't think the
Throwable
part should get any helpers beyond raising and lifting it to the typed channel (
IO<Nothing, A> -> IO<Throwable, A>
). You are right, this increases api surface, but I don't think
E
and
Throwable
should be treated equally as we do actually want users to prefer
E
. The whole thing is a balancing act, but it is a lot safer to simply catch all
s
IO<E, A>
<~>
IO<Nothing, Either<E, A>>
<~>
EitherT<ForIO, E, A>
<~>
IO<Either<E, A>>
<~>
suspend () -> Either<E, A>
. Merging
E
is
Throwable
is not valid, and is only possible at the cost of totality. In other words you loose referential transparency, and thus the substitution model and all we love about
IO
.
s
well, I would definitely go to a talk about "Designing IO<E, A> in Arrow" 😄 this was an interesting discussion for me, thanks
s
I am thinking about such a talk, but I want to include all of the above in the
IO<E, A>
documentation in the next couple weeks as well.
❤️ 2
This shouldn’t be info you have to discover on Slack, I want to include all this in the actual docs.
s
raiseException
<- you probably don't wanna use this, take a look at
raiseError
instead 😄