Cutover from imperative Try to IO

Hi,

A question about design … I have some code here: https://github.com/sageserpent-open/kineticMerge/blob/issue-1-clean-merging/src/main/scala/com/sageserpent/kineticmerge/Main.scala . It’s a command line tool.

The intent is to run a workflow of commands that change state in a Git repository, but abort immediately on a serious error, provide a custom error message that provides some context instead of an exception, and to rollback any changes made so far in the workflow prior to the error taking place.

When operations in the workflow succeed, logging entries are made and are shown on completion as a summary of what happened.

It works nicely enough, but has a bit of a hash of use of imperative Try expressions that are converted into a Workflow - that’s Cat’s EitherT layered on top of Writer. I’m bit uncomfortable with the mixing of Try with its immediate execution semantics with things like Writer that defer the logging in a nice clean functional way. If immediate execution is OK, why not simply print the logging entries directly in the imperative blocks?

Anyway, I thought I’d scratch an itch and refactor to using something like IO. My first thought was of a transformer version of IO, but I haven’t yet seen such a thing. What I’m left thinking of is an explicit …

private type Workflow[Payload] =
  IO[EitherT[WorkflowLogWriter, String @@ Tags.ErrorMessage, Payload]]

So the job is to cutover Try to IO, handle the exceptions with IO.handleError and spit out IO[EitherT[WorkflowLogWriter, String @@ Tags.ErrorMessage, Payload]].

I looked briefly at LiftIO but this would seem to do much the same as Try.toEither - my instincts tell me to stay in IO for as long as possible.

Rollback could be done with IO.bracketCase, I imagine.

Any suggestions from the floor?

Should I just stick with what I’ve got?

Be aware that there are some tricky issues with monad transformers that introduce extra side channels to IO, such as EitherT or WriterT. Though I guess since you have IO[EitherT[...]] instead of EitherT[IO, ...] you should still be fine.

1 Like

Cheers, @Jasper-M . I looked at that discussion and chuckled wryly. The do-nothing option seems a lot more tempting now :slight_smile:.

I notice though that a) they are wrapping the other way around - so WriterT/EitherT over IO and b) there is concurrency involved, and that’s what breaks the layering. It gives me an idea…

My use-case is a lot simpler, so I wonder whether I could get away with replacing Try with IO, using EitherT.liftAttemptK to get from straight IO to EitherT over IO and then left map the exceptions at that level. Worth a try, should be educational …

Following up, I tried it out, and it was pretty painless:

Passes the tests too. :smile_cat:

My opinions…

  • Thoroughly endorse your port from Try to IO.
  • Ive sworn off Writer for logging. When there’s an exception, when logs are most important, it throws the logs away. Useless!
  • I don’t think EitherT layers well with IO in practice. I just submerge my domain errors into Throwable and accept the loss of types, or I carry an Either in the effect payload over short distances.
1 Like

Thanks @benhutchison for the encouraging feedback. :smiling_face:

I’m OK here with EitherT[Writer(T), *, *] as you still get the logged entries even in the left channel of the eitherness part - tried this myself by injecting faults into the system, I see both logs of successful operations and the problem carried by the left channel.

However, from reading that discussion I can understand your wariness - there is a confusing fork between carrying an error explicitly in the eitherness or implied via IO’s participation in MonadError.

For me, the orginal code used Try to hoover up exceptions, then converted them asap into EitherT, replacing the orginal exception with a more descriptive error message to give the end user some idea of what the tool was trying to do, rather than some baffling exception. In the same vein I’m regarding the exception trapping of the initial IO instances as a springboard to the conversion - so once it’s lifted, the error message is carried by the eitherness, and the IOness is simply dealing with actual good values; the exception is cheerfully cast aside!