Request for code structuring feedback

tldr: I can write Haskell that works, but often think I could structure it better. I’d like some feedback and/or suggestions on how to improve some code.

Background

I’m at the stage with Haskell where I find I can generally write code that does what I want it to, but often find that it ends up bulky or difficult to test, or I find myself repeating more than I’d like to.

Although I have plenty of experience working in other languages, my knowledge of best practices in Haskell is lacking and I feel I might be missing some “jumps” of the form “If I’m writing this over and over I should consider refactoring to that”.

I’m currently working on a backend server for a website I manage which involves a concept of booking tickets for events. I’m using servant for the API structuring and polysemy for effect management.

The Problem

I find I end up writing server endpoints in a form like:

endpoint :: X -> Y -> Z -> Sem r a
endpoint x y z = do
  doSomething
    x
    y
    >>= \case
      ASuccessValue -> do
        logSomething
        doSomethingElse >>= \case
          ADifferentSuccessValue -> do
            ... etc
          ADifferentFailureValue err ->
            failureResponse $ "some message: " <> err
      AFailureValue err ->
        failureResponse err

This typically results in long chains of x >>= \case { ... } which can become quite hard to read, particularly when the actions within them are functions taking arguments spanning several lines.

My Thoughts on Approaches

I think a major part of it might be that I’m using sum types for a lot of pass/fail kind of cases, such as SendEmailSuccess | SendEmailFailure rather than Either. The reason for this is partly that if I were to refactor to use something like Either, I’m not sure how I could retain some of the injected debugging information for failures such as "some message" in the example above.

Another approach I’ve considered to build on that would be to use a custom monad with failure like Either, but map values such as SendEmailFailure to error values in that monad.

Request

I’d really appreciate feedback from anyone in the community who could point me in the direction of any patterns for this kind of problem which work in Haskell or what works for them, or if you have any comments on my ideas for approaches.

2 Likes

I think that you would want to write code like this:

endpoint :: X -> Y -> Z -> Sem r a
endpoint x y z = do
  doSomething
    x
    y
    `catch` failureResponse
  logSomething
  doSomethingElse
    `catch` (\err -> failureResponse $ "some message: " <> err)
  ... etc

This assumes that failureResponse is an effectful action that stops execution of the remaining actions, like you can do with Either and Maybe.

2 Likes

Thanks for this, I think I’ll be able to make it a lot cleaner using that as a starting point :smiley:

Declare more functions. Avoid infix stuff if you want to read your code a few months later.

Thank you both. Having implemented a suitable effect for generalising my fallible computations, I’ve ended up with a solution which, by keeping the flow more linear using catch-style infixes, means it’s a lot easier to extract functions cleanly.