Question

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?

Was it helpful?

Solution

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.

Licensed under: CC-BY-SA with attribution
scroll top