给定标记列表生成解析器

Generating a parser given a list of tokens

背景

我正在尝试使用 Parsec 实现日期打印和解析系统。

我成功实现了

类型的打印功能

showDate :: String -> Date -> Parser String

它会解析一个格式化字符串,并根据格式化字符串所呈现的标记创建一个新字符串。

例如

showDate "%d-%m-%Y" $ Date 2015 3 17

有输出Right "17-3-2015"

我已经编写了一个分词器用于 showDate 函数,所以我认为我可以使用它的输出以某种方式使用函数 readDate :: [Token] -> Parser Date 生成解析器。当我意识到我不知道如何实现它时,我的想法很快就停止了。

我想完成的事情

假设我们有以下函数和类型(实现无关紧要):

data Token = DayNumber | Year | MonthNumber | DayOrdinal | Literal String

-- Parses four digits and returns an integer
pYear :: Parser Integer

-- Parses two digits and returns an integer
pMonthNum :: Parser Int

-- Parses two digits and returns an integer
pDayNum :: Parser Int

-- Parses two digits and an ordinal suffix and returns an integer
pDayOrd :: Parser Int

-- Parses a string literal
pLiteral :: String -> Parser String

解析器 readDate [DayNumber,Literal "-",MonthNumber,Literal "-",Year] 应该等同于

do
     d <- pDayNum
     pLiteral "-"
     m <- pMonthNum
     pLiteral "-"
     y <- pYear
     return $ Date y m d

同样,解析器readDate [Literal "~~", MonthNumber,Literal "hello",DayNumber,Literal " ",Year]应该等同于

do
     pLiteral "~~"
     m <- pMonthNum
     pLiteral "hello"
     d <- pDayNum
     pLiteral " "
     y <- pYear
     return $ Date y m d

我的直觉表明我可以使用某种 concat/map/fold 使用 monad 绑定,但我不知道。

问题

parsec 是正确的工具吗?

我的方法是复杂的还是无效的?

您的 Token 是针对日期格式 [Token] 的小型语言的说明。

import Data.Functor
import Text.Parsec
import Text.Parsec.String

data Date = Date Int Int Int deriving (Show)

data Token = DayNumber | Year | MonthNumber | Literal String

为了解释这种语言,我们需要一种表示解释器状态的类型。我们开始不知道 Date 的任何组成部分,然后在遇到 DayNumberYearMonthNumber 时发现它们。下面的DateState代表了Date.

的各个组成部分知道或不知道的状态
data DateState = DateState {dayState :: (Maybe Int), monthState :: (Maybe Int), yearState :: (Maybe Int)}

我们将开始用 DateState Nothing Nothing Nothing 解释 [Token]

每个 Token 将被转换为一个函数,该函数读取 DateState 并生成一个解析器来计算新的 DateState.

readDateToken :: Token -> DateState -> Parser DateState
readDateToken (DayNumber) ds =
    do
        day <- pNatural
        return ds {dayState = Just day}
readDateToken (MonthNumber) ds =
    do
        month <- pNatural
        return ds {monthState = Just month}
readDateToken (Year) ds =
    do
        year <- pNatural
        return ds {yearState = Just year}
readDateToken (Literal l) ds = string l >> return ds

pNatural :: Num a => Parser a
pNatural = fromInteger . read <$> many1 digit

要读取解释 [Token] 的日期,我们首先将其转换为函数列表,这些函数决定如何根据 map readDateToken :: [Token] -> [DateState -> Parser DateState] 的当前状态解析新状态。然后,从初始状态 return (DateState Nothing Nothing Nothing) 成功的解析器开始,我们将把所有这些函数与 >>= 绑定在一起。如果生成的 DateState 没有完全定义 Date 我们将抱怨 [Token] 无效。我们也可以提前检查一下。如果您想将无效日期错误包含为解析错误,这也是检查 Date 是否有效并且不代表不存在的日期(如 4 月 31 日)的地方。

readDate :: [Token] -> Parser Date
readDate tokens =
    do
        dateState <- foldl (>>=) (return (DateState Nothing Nothing Nothing)) . map readDateToken $ tokens
        case dateState of 
            DateState (Just day) (Just month) (Just year) -> return (Date day month year)
            _                                             -> fail "Date format is incomplete"

我们将运行举几个例子。

runp p s = runParser p () "runp" s

main = do
    print . runp (readDate [DayNumber,Literal "-",MonthNumber,Literal "-",Year])                   $ "12-3-456"
    print . runp (readDate [Literal "~~", MonthNumber,Literal "hello",DayNumber,Literal " ",Year]) $ "~~3hello12 456"
    print . runp (readDate [DayNumber,Literal "-",MonthNumber,Literal "-",Year,Literal "-",Year])  $ "12-3-456-789"
    print . runp (readDate [DayNumber,Literal "-",MonthNumber])                                    $ "12-3"

这会产生以下输出。请注意,当我们要求阅读 Year 两次时,Date 中使用了两年中的第二个。您可以通过修改 readDateToken 的定义并可能修改 DateState 类型来选择不同的行为。当 [Token] 没有指定如何读取其中一个日期字段时,我们会收到错误 Date format is incomplete 和稍微不正确的描述;这可以改进。

Right (Date 12 3 456)

Right (Date 12 3 456)

Right (Date 12 3 789)

Left "runp" (line 1, column 5):
unexpected end of input
expecting digit
Date format is incomplete