Home > Software engineering >  How to restructure so as to not be stuck in the IO monad?
How to restructure so as to not be stuck in the IO monad?

Time:11-29

I'm creating a simple language for interactive date wrangling. I want to have a rule that stands for "today":

import Data.Time
import qualified Text.Parsec as Parsec
import Text.Parsec.String(Parser)

data Date = Date { year  :: Int
                 , month :: Int
                 , day   :: Int
                 }

parseToday :: Parser Date
parseToday = do
  Parsec.string "today"
  now <- getCurrentTime
  let (year, month, day) = toGregorian $ utctDay now
  return $ Date year month day

This doesn't work because getCurrentTime returns a monad of type IO UTCTime while the Parser is of type Parser Date. Certainly, getting the current time needs to be an IO action. However, is there no remedy against putting everything in the IO monad, for all the other Parsers that I have?

CodePudding user response:

One option is to make two types; one represents all the things you might need to parse, and the other represents a "compiled" version.

data ParsedDate = Today | Specified Date

parseToday :: Parser ParsedDate
parseToday = Today <$ Parsec.string "today"

compile :: ParsedDate -> IO Date
compile Today = do
    (year, month, day) <- toGregorian . utctDay <$> getCurrentTime
    return (Date year month day)
compile (Specified date) = return date

If you have a containing data structure that may have many ParsedDates in it, you may want to make sure that getCurrentTime is called only once, so that they relate to each other coherently even if the parser is run right as the clock is about to tick over from one day to the next. A second option that addresses that concern looks like this:

parseToday :: ParserT ((->) UTCTime) Date
parseToday = do
    Parsec.string "today"
    (year, month, day) <- asks (toGregorian . utctDay)
    return (Date year month day)

The downside of this approach is that you kind of need to do all your IO up front; if getCurrentTime is the only information you would ever need to gather, that's probably not a big deal, but if you may have more expensive IO operations that you'd like to only perform as needed, the first approach is better. If you need both once-only execution and as-needed execution, things get a bit more complex...

CodePudding user response:

Certainly, getting the current time needs to be an IO action. However, is there no remedy against putting everything in the IO monad, for all the other Parsers that I have?

...actually, there is one possible remedy - an abstract data type:

module Rio(Rio, runRio, currentTime) where
import Data.Time(UTCTime, getCurrentTime)

newtype Rio a = Rio (IO a)
  deriving (Applicative, Functor, Monad)
            -- ...whatever's needed.

runRio :: Rio a -> IO a
runRio (Rio a) = a

currentTime :: Rio UTCTime
currentTime = liftRio getCurrentTime



 -- local definitions: don't export these!
 --
liftRio :: IO a -> Rio a
liftRio = Rio

With a suitable type declaration e.g:

type Parser a = ParsecT String () Rio a

...your "today" rule would then look something like:

parseToday :: Parser Date
parseToday = do
  Parsec.string "today"
  now <- lift currentTime
  let (year, month, day) = toGregorian $ utctDay now
  return $ Date year month day

If you need other I/O operations, you can just add them to the Rio module.

  • Related