I'm currently learning Haskell. I'm stuck while composing a few IO functions with pure functions (I'm not sure if that is possible using the structure I'm using).
I have included below the full toy code replicating my actual problem.
import Data.Function ((&))
data MyType = I Int | S String | B Bool deriving (Show)
debugPrint :: Bool -> MyType -> IO (Either String MyType)
debugPrint flag mt =
if flag
then do
print mt
return (Left "Debug mode: short circuiting further steps")
else return (Right mt)
strToInt :: String -> Either String MyType
strToInt s =
if sLen >= 10
then Left "String is too long"
else Right (I sLen)
where
sLen = length s
intToBool :: MyType -> Either String MyType
intToBool (I i) =
if i <= 0
then Left "Integer is <= 0"
else Right (B (i > 5))
intToBool _ = undefined
boolToStr :: MyType -> Either String MyType
boolToStr (B b) =
if not b
then Left "True expected"
else Right (S "Final output")
boolToStr _ = undefined
finalHandler :: Either String MyType -> IO ()
finalHandler (Left err) = putStrLn $ "ERROR: " ++ err
finalHandler (Right myType) = print myType
main :: IO ()
main = do
s <- getLine
strToInt s
(_) debugPrint True
(_) intToBool
(_) debugPrint True
(_) boolToStr
(_) debugPrint True
& finalHandler
I have 3 questions about the above snippet.
main
.Left
value in the intermediate outputs, the control should jump to finalHandler
and let it process the value.debugPrint
with strToInt s
.
I want to pass the Right
value to debugPrint
and conditionally print it using the flag
parameter and return the same value if it's not being printed.Please ignore the undefined pattern matches as they are handled in my actual code.
I have tried using bind
, (>=>)
and liftIO
. But I'm still not able to how the abstraction can be changed to improve the data flow in the snippet.
There are a couple of ways this can be done, including explicit pattern matching. And of course you can always define your on operators for any task. Both are perhaps a good exercise.
However, what I would consider an idiomatic solution is to let all the work be done by the monad chaining.
I don't know how deep you've learned about monad; suffice it to say that the “mon” refers to a sense of singularity. It means that everything is one, so to say. Ok, that was a very nebulous statement; in practice what you should remember is that to monadically chain functions/actions, the result (i.e. to the right of all ->
) needs to have the same outer type-constructor in all of them. You already know this with e.g. IO Int
and IO ()
, but it works just as well with any other monad.
So that's what you need to achieve here, and then you can just use the ordinary >>=
/ do block syntax. First you need to figure out what's an appropriate monad into which you can cast all those actions. The simplest option would actually be to just use IO
, in which you can after all throw and catch exceptions. You could reqrite all of your functions to be in IO
– this, again, is a useful exercise but not idiomatic.
A more discipled option is to have the error cases as an explicit monad transformer on top of IO
. You might expect this to be called EitherT
, which indeed exists, but the more commonly used version is called ExceptT
. It's still the same, literally just a newtype wrapper around m (Either e a)
, which is a generalized form of e.g. the IO (Either String MyType)
you have in your code.
The point of this newtype wrapper is to avoid nesting Either ...
inside of IO ...
(two monads), and instead combining them both into a single monad, so again the old combinators can be used.
What you still need to get started is a way to lift your Either ...
results into the ExceptT
monad. The transformers
library has except
, which does exactly that.
For debugPrint
, I would change the type right in its signature:
debugPrint :: Bool -> MyType -> ExceptT String IO MyType
For the others, I wouldn't change the type but instead wrap them in except
when using them in main
.
I leave the actual code as an exercise.