I've run into the same scenario involving Monad Transformers a few times now, and I'm looking for some "best-practice" advice.
Bear with the introduction, the issue itself is really simple:
Say you're given a list of inputs, and you want to convert them into some aggregated form while handling the possibility of invalid inputs. In code, we could have something like:
type Input = String type ProcessedInput = String type Output = S.Set ProcessedInput data Error = InvalidFormat Input | DuplicateInput ProcessedInput processInput :: Input -> Maybe ProcessedInput -- Useful later, not vital to the problem ifM :: Monad m => m Bool -> m a -> m a -> m a ifM cond t f = cond >>= \c -> if c then t else f
and we want something like:
buildInputs :: [Input] -> Either Error Output
I've found that it's normally nicer to compute this inside a little isolated monad transformer stack (
StateT around an
Except, so we can build up our result and handle errors when needed) instead of doing a bunch of maps and
any checks and aggregations.
So the question: Is it "better" to express this computation as acting on single inputs, or acting on the entire input? Bearing in mind that this is an entirely self-contained computation, it won't need to be used as a subcomputation anywhere else.
Using a "single-input" action requires some slightly unintuitive wrapping in
buildInputs :: [Input] -> Either Error Output buildInputs is = runExcept $ execStateT builder S.empty where builder = sequence (map buildInputs' is) buildInputs' :: Input -> StateT Output (Except Error) () buildInputs' i = case processInput i of Nothing -> throwError (InvalidFormat i) Just p -> ifM (gets $ S.member p) (throwError $ DuplicateInput p) (modify $ S.insert p)
Using a "whole-input" action gives a cleaner wrapper at the cost of a messier body:
buildInputs :: [Input] -> Either Error Output buildInputs is = runExcept $ execStateT (buildInputs2' is) S.empty buildInputs' :: [Input] -> StateT Output (Except Error) () buildInputs'  = return () buildInputs' (i:is) = case processInput i of Nothing -> throwError (InvalidFormat i) Just p -> do ifM (gets $ S.member p) (throwError $ DuplicateInput p) (modify $ S.insert p) buildInputs' is
Is there a best-practice convention for this sort of thing? Is there an entirely better way of doing this that I've missed?