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
IOaction. However, is there no remedy against putting everything in theIOmonad, 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.
