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?