第 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)]
- 在本章中,我们只考虑解析器的返回序列长度为
0或1这两种情况
最后,为了能够将 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)]
itemparser 仅从程序中取出第一个字符
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 === xx <|> 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) 定义为满足如下条件的字符串:
-
字符串的首字符必须是一个小写英文字母
-
除首字符之外的其他字符,或者是英文字母,或者是数字
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 算术运算表达式的解析与评估
考虑满足如下条件的表达式:
-
仅包含
个位数、+、*、以及用于优先级控制的圆括号对() -
+和*满足右结合律 -
*的优先级高于+
这种表达式可以使用如下的 上下文无关文法 (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 | ε) }