在给定令牌列表的情况下生成解析器

时间:2015-03-18 02:22:12

标签: parsing haskell

背景

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

我已经成功实现了

类型的打印功能

showDate :: String -> Date -> Parser String

它需要解析格式化字符串,并根据格式化字符串显示的标记创建一个新字符串。

例如

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

的输出为Right "17-3-2015"

我已经编写了一个在showDate函数中使用的tokenizer,所以我认为我可以使用它的输出以某种方式使用函数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

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

问题

parsec是否是正确的工具?

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

  • 如果没有,我该如何实现此功能?

  • 如果是这样,我应该尝试做什么?

1 个答案:

答案 0 :(得分:4)

您的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)}

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

每个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] s无效。我们也可以提前检查一下。如果您希望将无效日期错误包括为解析错误,那么也可以检查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