-
-
Save ifesdjeen/4be994aea5846aa1c2fe to your computer and use it in GitHub Desktop.
| data Query = Query | |
| data SomeObj = SomeObj | |
| data IoOnlyObj = IoOnlyObj | |
| data Err = Err | |
| -- There's a decoder function that makes some object from String | |
| decodeFn :: String -> Either Err SomeObj | |
| decodeFn = undefined | |
| -- There's a query, that runs against DB and returns array of strings | |
| fetchFn :: Query -> IO [String] | |
| fetchFn = undefined | |
| -- there's some additional "context initializer", that also has IO | |
| -- side-effects | |
| makeIoOnlyObj :: [SomeObj] -> IO [(SomeObj, IoOnlyObj)] | |
| makeIoOnlyObj = undefined | |
| -- and now, there's a pipeline function, that takes query, | |
| -- decodes results, and then runs another IO operation with the | |
| -- results of query. And it seems to me that there are much much | |
| -- better ways of implementing such things. | |
| -- | |
| -- `makeIoOnlyObj` returns IO-wrapped result, and we need | |
| -- return IO Either-wrapped. | |
| -- | |
| -- So far what I've got is as follows. How do I improve it? | |
| pipelineFn :: Query | |
| -> IO (Either Err [(SomeObj, IoOnlyObj)]) | |
| pipelineFn query = do | |
| a <- fetchFn query | |
| case sequence (map decodeFn a) of | |
| (Left err) -> return $ Left $ err | |
| (Right res) -> do | |
| a <- makeIoOnlyObj res | |
| return $ Right a |
To complement YoEight's answer, here is a solution which does the same as yours, but shorter. This version of your code saves a few lines by injecting the makeIoOnlyObj and the decoding inside the query result using Functor properties of IO and Either Err.
pipelineFn = do
-- (a) turns the query into an error/an action
x <- (fmap makeIoOnlyObj . traverse decodeFn) <$> fetchFn query
-- (b) propagates the error or run the action
either (return . Left) (Right <$>) xI prefer YoEight's answer but I think this form is useful: in this form, line (b) illustrates why YoEight tells that, in such use case, your problem asks for a transformer. Line (b) tries to pop the action out of the Either Err while keeping the same error/no-error context (i.e., a good old try / catch block), which is the job of the ExcepT. In summary, my solution is a middle ground between the two other solutions, and may help you identify why a transformer is preferrable. If your codebase grows, the ExcepT saves you from typing the either (return . Left) (fmap Right) every time you chain actions (you'll pay in lifts, which are much cheapers because they do not expose internals (unlike 'either')).
Another variation of traverse version, suggested by @bitemyapp is:
pipelineFn :: Query
-> IO (Either Err [(SomeObj, IoOnlyObj)])
pipelineFn query = do
a <- fetchFn query
traverse makeIoOnlyObj (mapM decodeFn a)
In such use case, monad transformers is what you need. So I gonna use
ExceptTmonad transformer. It augments any Monad m to support failure.You can find it in transformers package
At some point, you'll need to evaluate your
ExceptTcomputation. All you have to do is usingrunExceptfunction.