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.
Is this an idiomatic way to segregate & catch partial computations?
Are there notable drawbacks to this high level segregation?
Are there better abstraction techniques?