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 ParsedDate
s 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 theIO
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.