Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 13 章:Monadic Parser


01 什么是解析器 (Parser) ?

解析器是一个程序:它接收一段文本信息 (即,一个字符串),对其进行分析,确定其语法结构 (Syntactic Structure)

例如,对于字符串 2 * 3 + 4,一个特定的解析器可能会把它理解为如下的树形结构:


02 在哪里会用到解析器 ?

目前看来,任何一种程序设计语言,它的工具链中大概都会存在一个解析器。

举例而言:

  • 你在 ghci 中输入的字符串 (Haskell 程序/表达式),需要经过一个解析器进行分析后,才会进行后续处理

  • 你在 终端 (Terminal) 中输入的命令 (也是一种程序),也是如此

  • 在浏览器中打开一个页面,本质上是打开一个使用 HTML 语言编写的程序,将这个程序解析为一棵 DOM (Document Object Model) 树,然后再把这棵树渲染在浏览器窗口中

  • 你现在正在浏览的这个页面,它的本质是一个使用 markdown 语法撰写的程序;这个程序在被解析后被转换为 HTML 程序,然后再被浏览器解析和渲染,然后才被你看到


03 将解析器建模为函数

type Parser = String -> Tree
  • 解析器是一个函数:它接收一个字符串,返回一种树形结构

一般情况下,解析器的输出是一棵树,称为 抽象语法树 (Abstract Syntax Tree / AST)


在更一般的情况下,我们需要表示 “部分解析”:

  • 即,只解析了输入字符串的一个前缀 (余下的部分仍然是一个字符串)
type Parser = String -> (Tree, String)
  • 解析器的返回值是一个二元组 (Tree, String)

    • 已被解析的部分被表示为一棵树

    • 未被解析的部分仍然保持字符串的形态


在更为复杂的情况下,可能会解析失败,也可能存在多种不同的解析方式:

type Parser = String -> [(Tree, String)]
  • 解析器的返回值是一个序列 [(Tree, String)]

    • 当解析失败时,返回序列的长度为 0

    • 当只存在一种解析方式时,返回序列的长度为 1

    • 当存在 n 种解析方式时,返回序列的长度为 n


然后,我们可以将解析器的输出泛化为任何一种类型:

type Parser a = String -> [(a, String)]
  • 在本章中,我们只考虑解析器的返回序列长度为 01 这两种情况

最后,为了能够将 Parser 声明为 Monad 的实例,对 Parser 进行如下定义:

newtype Parser a = P (String -> [(a,String)])

顺便定义一个 app 函数,将一个解析器作用到一个程序上:

app :: Parser a -> String -> [(a,String)]
app (P f) = f

下面,我们就来实现各种各样的解析器。


04 The item Parser

item :: Parser Char
item  = P $ \program -> case program of
                []     -> []
                (x:xs) -> [(x, xs)]

item parser 仅从程序中取出第一个字符

ghci> app item ""
[]

ghci> app item "abc"
[('a',"bc")]

05 将 Parser 声明为 Monad 的实例

instance Functor Parser where

 -- fmap :: (a -> b) -> Parser a -> Parser b
    fmap g p = P $ \program -> case app p program of
                       []         -> []  -- 遇到失败,则传播/返回失败
                       [(v, out)] -> [(g v, out)]
ghci> app (toUpper <$> item) "abc"
[('A',"bc")]

ghci> app (toUpper <$> item) ""
[]

instance Applicative Parser where

 -- pure :: a -> Parser a
    pure v = P $ \program -> [(v,program)]

 -- <*> :: Parser (a -> b) -> Parser a -> Parser b
    pg <*> px = P $ \program -> case app pg program of
                    []         -> []  -- 遇到失败,则传播/返回失败
                    [(g, out)] -> app (g <$> px) out
ghci> app (pure 1) "abc"
[(1,"abc")]

ghci> three = g <$> item <*> item <*> item where g x y z = (x,z)
ghci> app three "abcdef"
[(('a','c'),"def")]

instance Monad Parser where

 -- (>>=) :: Parser a -> (a -> Parser b) -> Parser b
    p >>= f = P $ \program -> case app p program of
                      []         -> []
                      [(v, out)] -> app (f v) out
ghci> app (return 1) "abc"
[(1,"abc")]

ghci> three = do { x <- item; item; z <- item; return (x, z) }
ghci> app three "abcdef"
[((‘a','c'),"def")]

06 选择 (Choice)

在模块 Control.Applicative 中定义了一个类簇:

-- A monoid on applicative functors.
class Applicative f => Alternative f where

    -- An associative binary operation
    -- 一个满足结合律的二元运算符
    (<|>) :: f a -> f a -> f a

    -- The identity of '<|>'
    -- 二元运算符 '<|>' 的单位元
    empty :: f a

    -- Zero or more.
    many :: f a -> f [a]
    many v = some v <|> pure []

    -- One or more.
    some :: f a -> f [a]
    some v = (:) <$> v <*> many v
  • <|> 满足结合律

    x <|> (y <|> z) === (x <|> y) <|> z
    
  • empty<|> 的单位元

    empty <|> x === x
    
    x <|> empty === x
    

Maybe 声明为 Alternative 的实例

instance Alternative Maybe where

 -- empty :: Maybe a
    empty = Nothing

 -- (<|>) :: Maybe a -> Maybe a -> Maybe a
    Nothing <|> r = r  -- 若第一个选择为空,则返回第二个选择
    l       <|> _ = l  -- 若第一个选择非空,则返回第一个选择

 -- 以下代码无需书写;放在这里,只为方便阅读和理解

 -- Zero or more.
    many :: Maybe a -> Maybe [a]
    many v = some v <|> pure []
    --                  ^^^^^^^
    --                  === Just []
    --       如果 some v 为 Nothing,则返回 Just [];表示 zero 次
    --       否则,返回 some v;表示 more 次

 -- One or more.
    some :: Maybe a -> Maybe [a]
    some v = (:) <$> v <*> many v
    -- 若 v 为 Nothing,则返回 Nothing
ghci> import Control.Applicative

ghci> some Nothing
Nothing

ghci> many Nothing
Just []

Parser 声明为 Alternative 的实例

instance Alternative Parser where

 -- empty :: Parser a
    empty = P $ \program -> []
    -- 一个直接返回失败的解析器

 -- (<|>) :: Parser a -> Parser a -> Parser a
    p <|> q = P $ \program -> case app p program of
                      []  -> app q program
                      rst -> rst
    -- 对于输入的程序,首先用 p 进行解析:
       -- 如果解析失败,则用 q 进行解析,并返回解析的结果
       -- 否则,返回用 p 进行解析的结果
    --
    -- 简而言之,p <|> q 的效果是:
       -- 如果一个程序用 p 能够成功解析,则 p <|> q === p
       -- 否则,p <|> q === q
    --
    -- 这样,就实现了一种顺序尝试的效果:
       -- 即,顺序尝试若干解析方式
          -- 遇到第一个成功的解析方式,则返回该解析结果
          -- 否则,返回最后一种解析方式的结果 (无论成功或失败)

 -- 以下代码无需书写;放在这里,只为方便阅读和理解

 -- Zero or more.
    many :: Parser a -> Parser [a]
    many v = some v <|> pure []
    --                  ^^^^^^^
    --                  === P $ \program -> [([],program)]
    --
    -- 对输入的程序尽可能多地连续使用 v 进行解析
       -- 若一次都没有成功,则不进行任何解析

 -- One or more.
    some :: Parser a -> Parser [a]
    some v = (:) <$> v <*> many v
    -- 首先,对输入的程序使用 v 进行一次解析
       -- 若发生失败,则返回/传播失败
    -- 然后,再对余下未被解析的程序 使用 many v 进行解析
ghci> app empty "abc"
[]

ghci> app (item <|> return 'd') "abc"
[('a',"bc")]

ghci> app (empty <|> return 'd') "abc"
[('d',"abc")]

07 若干基础解析器

sat :: (Char -> Bool) -> Parser Char
sat p = do
    x <- item
    if p x then return x else empty

-- 或者
sat p = item >>= \x -> if p x then return x else empty
  • sat p 把两个动作组合在一起

    • 首先,解析出程序中的第一个字符 x

    • 然后,如果 x 满足谓词 p

      • 则返回 x 以及余下未被解析的程序

      • 否则,返回失败


-- 数字字符解析器
digit :: Parser Char
digit  = sat isDigit

-- 小写字母解析器
lower :: Parser Char
lower = sat isLower

-- 大写字母解析器
upper :: Parser Char
upper = sat isUpper

-- 字母解析器
letter :: Parser Char
letter = sat isAlpha

-- 字母或数字解析器
alphanum :: Parser Char
alphanum = sat isAlphaNum

-- 指定字符解析器
char  :: Char -> Parser Char
char x = sat (x ==)

课堂练习:

定义一个解析器:

string :: String -> Parser String

分析输入的程序是否具有一个制定的前缀。

string 的行为示例如下:

ghci> app (string "abc") "abcdef"
[("abc","def")]

ghci> app (string "abc") "ab1234"
[]

ghci> app (string "") "ab1234"
[("","ab1234")]
#![allow(unused)]
fn main() {
string :: String -> Parser String
string [] = return []
string (x:xs) = do
    char x
    string xs
    return (x:xs)
}

08 The ident Parser / 标识符解析器

我们将一个标识符 (identifier) 定义为满足如下条件的字符串:

  1. 字符串的首字符必须是一个小写英文字母

  2. 除首字符之外的其他字符,或者是英文字母,或者是数字

ident :: Parser String
ident = do
    x  <- lower
    xs <- many alphanum
    return (x:xs)
ghci> app ident "abc def"
[("abc"," def")]

ghci> app ident "12 def"
[]

09 The nat Parser / 自然数解析器

nat :: Parser Int
nat = do
    xs <- some digit
    return (read xs)
ghci> app nat "123abc"
[(123,"abc")]

ghci> app nat "abc123"
[]

10 The space Parser / 空格字符解析器

space :: Parser ()
space = do
    many (sat isSpace)
    return ()
ghci> app space "   abc"
[((),"abc")]

11 The int Parser / 整数解析器

int :: Parser Int
int = do char '-'
         n <- nat
         return $ - n
      <|> nat
ghci> app int "123abc"
[(123,"abc")]

ghci> app int "-123abc"
[(-123,"abc")]
ghci> app int "abc123"
[]

12 在解析过程中,去除首尾空格

token :: Parser a -> Parser a
token p = do
    space
    v <- p
    space
    return v
identifier :: Parser String
identifier = token ident

natural :: Parser Int
natural = token nat

integer :: Parser Int
integer = token int

symbol :: String -> Parser String
symbol xs = token $ string xs

13 The nats Parser

nats :: Parser [Int]
nats = do
    symbol "["
    n <- natural
    ns <- many $ do {symbol ","; natural}
    symbol "]"
    return (n:ns)
ghci> app nats "[1, 2, 3 ]"
[([1,2,3],"")]

ghci> app nats "[1, 2, 3, ]"
[]

14 算术运算表达式的解析与评估

考虑满足如下条件的表达式:

  1. 仅包含 个位数+*、以及用于优先级控制的圆括号对 ( )

  2. +* 满足右结合律

  3. * 的优先级高于 +


这种表达式可以使用如下的 上下文无关文法 (Context-Free Grammar) 进行描述

#![allow(unused)]
fn main() {
expr   ::= term '+' expr | term
// 一个 expr,
// - 或者是:一个 term 后跟一个字符 '+',然后再跟一个 expr
// - 或者是:一个 term
//
// 其中:
// - 用单引号包围的字符,称为 终结字符 (即,出现在程序中的固定字符)
// - 字符 | 是一种表示 “或” 的结构
//
// 上面这一条文法,也可以紧凑地表示为如下形式
expr   ::= term ('+' expr | ε) // 其中,ε 表示 “空”

term   ::= factor '*' term | factor
// 类似地,上面这条文法也可写为如下紧凑形式
term   ::= factor ('*' term | ε)

factor ::= digit | '(' expr ')'

digit  ::= '0' | '1' | ... | '9'
}

下面,我们就把上面的文法依次翻译到对应的解析器上。


#![allow(unused)]
fn main() {
expr ::= term ('+' expr | ε)
}
expr :: Parser Int
expr  = do t <- term
           do   symbol "+"
                e <- expr
                return (t + e)
            <|> return t

#![allow(unused)]
fn main() {
term ::= factor ('*' term | ε)
}
term :: Parser Int
term  = do f <- factor
           do   symbol "*"
                t <- term
                return (f * t)
            <|> return f

#![allow(unused)]
fn main() {
factor ::= digit | '(' expr ')'
}
factor :: Parser Int
factor  = do   symbol "("
               e <- expr
               symbol ")"
               return e
           <|> natural

最后,定义评估函数:

eval :: String -> Int
eval xs = fst $ head $ app expr xs

下面是使用示例:

ghci> eval "2 * ( 3 + 4 )"
14

ghci> eval "2 * 3 + 4"
10

本章作业

作业 01

对本章介绍的算术表达式解析器进行扩展,支持 减 (-)除 (/) 两种运算。

具体而言,根据如下两条更新后的文法,对解析器的实现进行相应地修改:

#![allow(unused)]
fn main() {
expr ::= term ('+' expr | '-' expr | ε)

term ::= factor ('*' term | '/' term | ε)
}