8

After developing several Haskell applications I've found myself rigorously segregating impure code and failable (partial) functions from their pure & total counterparts. These efforts have noticeably reduced maintenance cost associated with the applications. I have found myself over time relying on the same high level main structure to enforce this segregation.

In general, my main will have the following structure:

import System.Environment

data ProgramParameters = P ()
data ComputationResult = I ()

main :: IO ()
main = getArgs                           -- Collect arguments
   >>= andOrGetUserInput                 -- Collect user input
   >>= impureOrFailableComputations      -- Possible non-recoverable error(s)
   >>= either                            -- "Branch"
         putStrLn                        -- Print Any Failure(s)
         pureNotFailableComputations     -- Finish the work

andOrGetUserInput :: [String] -> IO ProgramParameters
andOrGetUserInput = undefined

impureOrFailableComputations :: ProgramParameters -> IO (Either String ComputationResult)
impureOrFailableComputations = undefined -- a composition of partial functions
                                         -- made total by catching exceptions & input errors
                                         -- in the short-circuiting ErrorT/EitherT monad

pureNotFailableComputations :: ComputationResult -> IO ()
pureNotFailableComputations = undefined  -- a composition of total functions

The goal is to coalesce partial computations in a monad, creating a total monadic computation.

This has become a pattern in the code base, and I would like feedback on whether this is a design pattern or an anti-pattern.

  • Is this an idiomatic way to segregate & catch partial computations?

  • Are there notable drawbacks to this high level segregation?

  • Are there better abstraction techniques?

7

This design makes several non-trivial assumptions:

  • The user's input will not depend on the results of pure or impure computations.

  • The impure computations will not depend on the result of pure computations.

  • The program logic will not loop; it will run only once.

My other comment on your structure is that you do not need to separate the pure and impure calculations. Haskell's type system already does that for you.

That said, this structure certainly looks useful for certain classes of programs, especially if you're sure the assumptions I outlined above actually hold true for your program. It's not something every program should use, though.

  • Also, if you want to simplify your pattern, consider that andOrGetUserInput almost certainly counts as an impureOrFalliableComputation. You can probably combine them into one section. – WolfeFan Sep 18 '14 at 18:55
  • In practice, they are condensed; for demonstration & exposition, I separated them. – recursion.ninja Sep 18 '14 at 19:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.