上期链接:用Haskell构建Parser Combinator(一)
回顾
上篇文章从Parser
的类型、Parser
的最简实现,一步步讲到Parser
的Monad
和相对完整的Parser
库。
实际上,该Parser
已经可以完成大部分的解析工作了。然而,当解析错误时,此Parser
仅仅简单地返回Nothing
。
当我们需要更为丰富的错误信息时,则需要在原来Parser
的基础上,增加相应的模块。
类型修改
还记得(一)中,Parser
类型是:
既然需要错误信息,就不能简单地返回Nothing
,而应该是一个String
。因此需要把类型改成Either
/Except
。
添加错误信息
satisfy
在satisfy
实现中添加错误信息:
测试:
Monad
把这些instance
改一下,就完成了程序的修改。
|
|
测试:
进一步
以上的“错误信息”虽聊胜于无,但是我们不应该就此满足。一份能用的错误信息,应该是具体的、有效的、能给用户以适当指示的。
因此,最好能够加上位置信息,以及能够自由地给出和修改提示。
接下来,让我们进一步修改类型。
位置信息
平时我们使用编译器、解释器,代码中存在语法错误时,编译器、解释器的Parser
总能正确地指出错误所在位置,使我们不必迷失在代码的海洋中不知所措(C++ template metaprogramming除外)。
考虑到添加位置信息的需求,那就需要考虑位置信息的存在、更新方式。我们这里的做法是,把位置信息与待解析字符串捆绑在一起,形成PString
,消耗字符串的时候,位置信息同步更新。PString
类型:
错误信息
有了位置信息,接下来我们就可以修改错误信息了。原本错误信息类型是简单的String
,现在使用自定义的错误信息类型:
DefaultError
和原来的差不多,仅仅封装了String
。EndOfInput
只需要装上位置信息。Chatty
同时需要位置信息和错误信息,以便于精确控制错误信息。
重造Parser类型
|
|
以上便是最终的Parser
类型。
用上了Monad Transformer
,有以下好处:
- 便于结合多种
Monad
。 - 让编译器自动实现各种范畴。仅仅需要在文件头加上:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
此时,由于使用了ExceptT
,通过throwError
和catchError
,我们就可以非常方便地添加和修改错误信息。
Alternative
还记得上篇文章中提到,<|>
的实现是强制回溯的。现在有了catchError
,我们可以实现不回溯的<|>
版本。当需要回溯时,可以使用try
。
|
|
直接catch
p1
的失败,这时PString
已经消耗,若此时再让p2
去解析,回溯就不会发生。
try
的实现:
get
把PString
拿到,如果p
解析失败了,就把原来的PString
放回去然后抛出错误,从而实现了回溯。
Position
待解析字符串和位置信息要同时更新,因此我们要重新实现satisfy
,以满足需求:
注意到'\n'
, '\t'
以及其他字符的区别。
runParser与错误信息输出
|
|
注意到runParser
中,待解析字符串和位置(1, 1)
结合。showErr
中,Chatty
和EndOfInput
错误都有对应的Pretty Print
。
把这些都封装在parse
函数中,转换成IO ()
类型:
添加错误信息
由于ExceptT
带有catchError
,可以利用它来提供更丰富的错误信息。
首先实现通用的catchChattyError
,它的作用是catch
到ChattyError
,添加上我们指定的额外错误信息。
接着恰当修改之前实现过的所有Parser
,在末尾加上更有用的信息:
测试:
达成任务。
下一步
- 现实例子,比如解析
JSON
类型。