0
votes

I'm converting some Python code to Haskell. The business logic is fairly complex and my Haskell code is getting ugly.

My Python function:

def f(some_state):
    doAction1() # e.g. send_message("Hi there!")
    if not userIsAuthenticated:
        doAction2() # e.g. send_message("Please login")
        return
    if not userHasPermission:
        doAction3() # e.g. send_message("Please sudo")
        return
    if somePredicate3:
        doAction4()
        if somePredicate4:
            doAction5()
    return

I have two constraints for the Haskell version:

  1. In order to keep my code as pure possible, I'd like to collect all my actions in a list (or a Free Monad if I'm feeling fancy) and then (outside the function) execute it. In Python, I would defined tmp = [] at the beginning of my function, append to tmp all my actions (without calling them) and return tmp. Then, I'd iterate over the list to execute all the actions (not Pythonic at all!).
  2. Not end up with pushed-to-the-right craziness like this:

-

if a
then undefined
else if b
     then undefined
     else if c
          then undefined
          else if d
               then undefined
               else undefined

Basically, I want to collect a list of actions and exit early if necessary, so that I can write something like:

tell Action1
when somePredicate1 $ tell doAction2
when somePredicate2 $ tell doAction3
...

tell is NOT from the writer monad but it's a function (which doesn't exist) that aggregates everything that has been "told" so far (à la writer monad) and is returned when I exit.

This really feels like a generalized Either monad, except that I need to keep all the actions that happened before the first Left (and not just the Left value) and then exit early at the first Left. I think the solution probably builds on MonadPlus or Alternative somehow, but I just can't figure out something nice. Basically, a fancy guard.

It's also definitely possible that I'm having too much fun over-engineering this and I should just accept that my code has to drift right. But that wouldn't be elegant, would it?

Edit: For clarity, I want a function of type f :: State -> [Action], where data Action = Action1 | Action2 | Action3.

1

1 Answers

2
votes

This sounds like an ideal use of throwError, from mtl's Control.Monad.Except, like so:

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RecordWildCards  #-}

module Main where

import Control.Monad.Except

data UnlockError =
    UnlockErrorNoLogin
  | UnlockErrorNeedsSudo

data State = State {
    stateUserIsAuthenticated :: Bool
  , stateUserHasPermission :: Bool
  }

unlock :: MonadError UnlockError m => State -> m String
unlock State{..} = do
  action1
  unless stateUserIsAuthenticated
         (throwError UnlockErrorNoLogin)
  unless stateUserHasPermission
         (throwError UnlockErrorNeedsSudo)
  return "all"
  where
    action1 = return ()

main :: IO ()
main = do
  state <- conjureState
  case unlock state of
    Left UnlockErrorNoLogin ->
      p "please log in"
    Left UnlockErrorNeedsSudo ->
      p "please sudo"
    Right count ->
      p ("i love you with " ++ show count ++ " of my heart")
  where
    p =
      putStrLn
    conjureState =
      return (State True True)