Search code examples
haskelliocomposition

Is there a way to chain pure and IO functions in Haskell?


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.

  • I would like to know if there are any predefined operators which can be used to chain the above functions in main.
  • Is there a better way to represent the error flow in this code? If there is any Left value in the intermediate outputs, the control should jump to finalHandler and let it process the value.
  • The main issue I'm dealing with is composing the function 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.


Solution

  • 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.