3

I am trying to get a good grip on the do notation in Haskell.

I could use it with Maybe and then print the result. Like this:

maybeAdd :: Maybe Integer
maybeAdd = do one <- maybe1
              two <- maybe2
              three <- maybe3
              return (one + two + three)

main :: IO ()
main = putStr (show $ fromMaybe 0 maybeAdd)

But instead of having a separate function I am trying to use the do notation with the Maybe inside the main function. But I am not having any luck. The various attempts I tried include:

main :: IO ()
main = do one <- maybe1
          two <- maybe2
          three <- maybe3
          putStr (show $ fromMaybe 0 $ return (one + two + three))
main :: IO ()
main = do one <- maybe1
          two <- maybe2
          three <- maybe3
          putStr (show $ fromMaybe 0 $ Just (one + two + three))
main :: IO ()
main = do one <- maybe1
          two <- maybe2
          three <- maybe3
          putStr (show $ (one + two + three))

All of these leads to various types of compilation errors, which unfortunately I failed to decipher to get the correct way to do it.

How do I achieve the above? And perhaps maybe an explanation of why the approaches I tried were wrong also?

Daniel
  • 26,899
  • 12
  • 60
  • 88
Finlay Weber
  • 2,989
  • 3
  • 17
  • 37
  • 1
    For this very reason there is the [Monad Transformers](https://en.wikibooks.org/wiki/Haskell/Monad_transformers) abstraction. For your case namely `MaybeT IO ()`. Then you will have a single do block. – Redu Apr 11 '20 at 09:02
  • Thanks ~Redu for chimming in. I am guessing MaybeT lives in another module somewhere. Where would that be? and how will it's usage look like? Will main now be of type MaybeT IO () ? I do not think that will be correct – Finlay Weber Apr 11 '20 at 09:19
  • I tried to address your questions by answering below. – Redu Apr 11 '20 at 13:32

2 Answers2

7

Each do block must work within a single monad. If you want to use multiple monads, you could use multiple do blocks. Trying to adapt your code:

main :: IO ()
main = do -- IO block
   let x = do -- Maybe block
          one <- maybe1
          two <- maybe2
          three <- maybe3
          return (one + two + three)
   putStr (show $ fromMaybe 0 x)

You could even use

main = do -- IO block
   putStr $ show $ fromMaybe 0 $ do -- Maybe block
      one <- maybe1
      two <- maybe2
      three <- maybe3
      return (one + two + three)
   -- other IO actions here

but it could be less readable in certain cases.

chi
  • 111,837
  • 3
  • 133
  • 218
  • 3
    `Each do block must working with a single monad` - I think that is the part I was missing. Thanks for pointing this out! – Finlay Weber Apr 11 '20 at 08:41
3

The MaybeT monad transformer would come handy in this particular case. MaybeT monad transformer is just a type defined something like;

newtype MaybeT m a = MaybeT {runMaybeT :: m (Maybe a)}

Actually transformers like MaybeT, StateT etc, are readily available in Control.Monad.Trans.Maybe, Control.Monad.Trans.State... For illustration purposes it' Monad instance could be something like shown below;

instance Monad m => Monad (MaybeT m) where
  return  = MaybeT . return . Just
  x >>= f = MaybeT $ runMaybeT x >>= g
            where
            g Nothing  = return Nothing
            g (Just x) = runMaybeT $ f x

so as you will notice the monadic f function takes a value that resides in the Maybe monad which itself is in another monad (IO in our case). The f function does it's thing and wraps the result back into MaybeT m a.

Also there is a MonadTrans class where you can have some common functionalities those are used by the transformer types. One such is lift which is used to lift the value into a transformer according to that particular instance's definition. For MaybeT it should look like

instance MonadTrans MaybeT where
    lift = MaybeT . (liftM Just)

Lets perform your task with monad transformers.

addInts :: MaybeT IO ()
addInts = do
          lift $ putStrLn "Enter two integers.."
          i <- lift getLine
          guard $ test i
          j <- lift getLine
          guard $ test j
          lift . print $ (read i :: Int) + (read j :: Int)
          where
          test = and . (map isDigit)

So when called like

λ> runMaybeT addInts
Enter two integers..
1453
1571
3024
Just ()

The catch is, since a monad transformer is also a member of Monad typeclass, one can nest them indefinitelly and still do things under a singe do notation.

Edit: answer gets downvoted but it is unclear to me why. If there is something wrong with the approach please care to elaborate me so that it helps people including me to learn something better.

Taking the opportunity of being on the edit session, i would like to add a better code since i think Char based testing might not be the best idea as it will not take negative Ints into account. So let's try using readMaybe from the Text.Read package while we are doing things with the Maybe type.

import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Class (lift)
import Text.Read (readMaybe)

addInts :: MaybeT IO ()
addInts = do
          lift $ putStrLn "Enter two integers.."
          i <- lift getLine
          MaybeT $ return (readMaybe i :: Maybe Int)
          j <- lift getLine
          MaybeT $ return (readMaybe j :: Maybe Int)
          lift . print $ (read i :: Int) + (read j :: Int)

I guess now it works better...

λ> runMaybeT addInts
Enter two integers..
-400
500
100
Just ()

λ> runMaybeT addInts
Enter two integers..
Not an Integer
Nothing
Redu
  • 25,060
  • 6
  • 56
  • 76
  • 3
    While I get what you are aiming for, I feel this answer would be clearer if, at least in its initial example, it tried to match the code in the question more closely, leaving out the `guard`s and `getLine`s. As a minor style suggestion, it might be nicer to use `liftIO` instead of `lift` to lift `IO` actions, as it translates more easily to *mtl* code, or to using multiple transformer layers on the top of `IO`. – duplode Apr 11 '20 at 19:12
  • 1
    @duplode Fair enough.. I have just tried to explain the transformers abstraction as much as i can and as far as i have understood what OP was probably asking for and will eventually have to learn. Anyways if the code is fine i am happy with my downvotes since it is a part of my learning practice as well. – Redu Apr 11 '20 at 19:22