解析器组合器是用于以可组合方式构建解析器的功能集。 Haskell的Parsec库和OCaml的Angstrom是两个示例。这两个库都公开了用于描述上下文相关语法的monadic接口。这篇文章着眼于实现一个更受限制的解析库,该解析库是围绕applicativefunctors而不是monads构造的。
有什么理由可以放弃单子?根据设计,可能会得到一些回报。在本练习中,目标是具有以下功能的API:
要查看为什么(1)无法用monad完成,请考虑使用monadic bind-operator构造的解析器:
在此,f是形式为' a->的函数。 &#39b解析器;这意味着我们不知道它将生成哪种解析器,直到为其提供值为止。因此,我们无法推断出这些解析器消耗的所有可能的符号。同样的道理适用于漂亮的印刷。
解析器将字符列表作为输入,并在成功时返回解析的值以及其余的输入。
但是,这种表示形式不支持符号提取,漂亮的打印或允许多种评估策略。为了适应这些情况,我们必须在更多上下文中修饰类型。问题是我们不一定知道完整的功能集每次需要一些新功能时(无论是错误报告,记录还是其他),我们都必须返回并更改定义以适应新功能。
替代方法不是尝试预先预期所有用例,而是选择一种保留尽可能多的结构的表示形式,以便稍后可以添加替代解释器。为此,我们将通过定义表示抽象语法树(AST)的类型,来有效地枚举解析器的集合及其组合方式。为此,我们将使用GADT,它还表示不同的解析器按不同的类型编制索引。初始构造函数分为两组-原始构造函数(树的叶子节点)和用于构成解析器的组合器:
输入' a t =(*本机解析器*)|失败:字符串-> ' t |空:单位t |返回:' a-> ' t |符号:char-> char t(*组成部分-适用*)|地图:(' a->' b)*' t-> '吨|产品:' t *' b t-> (' a *' b)t(*组成部分-替代*)|要么:' t *' t-> '在
返回x-不消耗任何输入并且始终返回x的解析器。
应用接口是提供顺序组合的接口,例如:首先使用解析器p1解析x,然后使用解析器p2解析y并将其结果合并。
Either构造函数可以提供替代的执行路径,即在不同类型的输入上成功的解析器。
由于我们不想让用户直接访问该类型,因此我们将机械地添加一些智能构造函数:
let empty =空let失败m =失败m let return x =返回x let symbol c = Symbol c let map fx = Map(f,x)let product pq = Product(p,q)让pq = Either(p, q)
拥有应用接口意味着还可以利用新的语法扩展-let + ..和+表示法-我在这里已经进行了描述。假设OCaml 4.08或dune future_syntax节,我们可以添加一个语法模块,如下所示:
模块语法= struct let(let +)p f =映射f p let(and +)p q =乘积p q结束
除此之外,我们还将包括一个带有infix版本的Ops模块以及一些派生的组合器:
module Ops = struct open语法let(< $>)fp = map fp let(< |>)pq =要么pq let(< *>)pf px = let + f = pf和+ x = fx中的px let(*>)pq =(fun _ x-> x)< $> p * q let(< *)p q = const< $> p * q结束
(*身份*)val id:' a-> '一个(* Const *)val const:' a-> ' b-> ' a(*正向构成*)val(>):(' a->' b)-> (' b->' c)-> ' a-> ' c(*将字符列表转换为字符串*)val string_of_list:字符列表->字符串(*然后再返回*)val list_of_string:字符串->字符列表
我们如何使用API构建实际的解析器?让我们考虑一些简单的例子。首先,解析器解析一个特定的字符串,即一个函数:
要实现字符串解析器,我们可以使用符号原语并在给定的字符串上折叠以使用适用性的语法(let + ..和+)组合解析器:
let字符串s = let accum cp =(*使用' c'解析器*)解析let + x =符号c(*然后解析' xs&#39 ;使用'解析器*)和+ xs = p(*将&#39&x39&xs'在字符串中组合*)Printf。 sprintf"%c%s" x xs在List中。 fold_right accum(list_of_string s)(return"")
接下来,让我们尝试一个解析器来解析数字。检测到一位数字的版本可以定义为:
在解析器列表之间进行选择是对二进制或组合器的自然概括,并应使用其自己的版本:
我们也可以泛化数字定义以将符号列表作为参数:
接下来,考虑一个可以识别任意整数的解析器?整数至少包含一个数字,但我们不知道确切多少个数字。我们如何定义捕获此语义的解析器?第一次尝试:
let int = let rec digits()= let + d = digit和+ ds = any(digits())(return [])in d :: ds in map(string_of_list>>< int_of_string)@@ digits()
您能发现以上定义的问题吗?如果没有,请尝试运行它,您会发现它引发了堆栈溢出异常。问题在于,递归调用永远不会以任何基本情况为条件,并且始终会对其进行评估。我们需要一种方法来描述可能会消耗任意大输入而无需构造无限解析表达式的解析器!为了从int示例中进行概括,我们的目标是具有以下签名的组合器:
输入' a t = ... |延迟:(单位-> a t)-> ' t t延迟f =延迟f
那是一个构造延迟的解析器。我们可以使用延迟来定义许多,如:
let rec many p = let many_one = let + x = p和+ xs = delay @@ fun _-> x :: xs中的many p)在many_one中的任何一个(return [])
现在,递归的每个步骤都是按需评估,而不是前期评估。如果该解决方案不是针对更雄心勃勃的约束集而必须进行漂亮的打印和符号提取,则此解决方案将非常有效。问题在于,为了提取延迟解析器的所有可能符号,我们需要对其进行评估;这将展开许多定义中表示的无限递归,并再次杀死堆栈。
对于同时具有有限可遍历表示并提供足够的表达能力来描述无限解析器的问题,是否有解决方法?问题就出在这里。不递归地表示递归结构的一种方式正是定点组合器所提供的。
让我们使用定点构造函数扩展解析器类型(并摆脱Delay):
输入' a t = ... |修复:(' a t->' a t)-> ' a t让修复f =修复f
然后,我们可以从上面使用fix作为对many定义的递归性的一种补救措施:
让很多p =修复@@有趣-> let many_one = let + x = p和+ xs = many in x :: xs在many_one中(return [])
请注意,看不到rec关键字。函数修复只是使我们能够模仿递归函数的便捷工具。假设我们仍然遇到需要评估的Fix节点-类型为(< a t->' a t)的函数,那么您可能会疑惑如何精确地解决符号提取问题。希望有关解释分析器的下一部分将使该问题更清晰。
回到整数解析的例子,这里是如何以许多方式定义int的:
let int = let + d =数字和+ ds = int_of_string @@ string_of_list(d :: ds)中的许多数字
同样,我们可以通过添加新的组合器来提取使用相同解析器(在上面的示例中为一个或多个数字)来解析一次或多次的通用模式,如下所示:
让many_one p =让+ x = p和+ xs =很多p x :: xs
给定一个解析器p,如果解析器p至少可以在其输入上应用p一次,则很多p成功。
附带说明一下,该代码慷慨地利用了新的应用语法来演示如何使用它来编写声明性代码,从而避免使用infix运算符。另外,我们也可以不依赖语法扩展而定义相同的函数。例如:
(*需要打开Ops模块*)让很多p =修复@@有趣->任一个(List。cons< $> p< *> many)(return [])let many_one p = List。缺点< $> p * many p let int =(string_of_list>> int_of_string)< $> many_一位数字
(*需要打开Ops模块*)let int =(string_of_list>> int_of_string string_of_list)< $> many_一位数字
为了总结最初的解析器示例集,这是一个浮点解析器的实现,它也支持以科学计数法解析值:
let float =(* Ex:123. *)let p1 = let + ds = many_one位,+ d =符号'。在ds @ [d]中(* Ex:123.45 *)中let p2 = let + ds1 = p1和+ ds2 = ds1中的many_one位数@ ds2 in(* Ex:12.34e56或12e34 *) p2< |> many_one位和+ e =符号' e'和+ ds2 = ds1 @ [e] @ ds2中的many_one数字,fol cs = float_of_string @@ string_of_list cs in fol< $>选择[p1; p2; p3]
到目前为止,重点一直是设计一种表达能力足够强的解析器语言,我们甚至已经实现了一些简单的示例,用于解析十进制数字。但是,关于如何在实际输入上运行解析器,没有一行代码。我们还没有对外观的确切语义做出任何决定,例如,运行解析器是否应该返回所有可能结果的列表,或者仅返回它找到的第一个有效结果?我们是否希望解析器回退一个或更多步骤以防卡住?
缺乏这样的设计决策是一个功能,而不是错误!精心设计用于构造解析器的前端,使我们能够提供多个后端来运行它们。首先,我们将看一个简单的非回溯评估器。
为了执行解析器,我们需要将其转换为函数。对于不处理错误报告的简单版本,我们可以使用与上述相同的类型-函数类型:char list-> (&*字符列表)选项:
我添加了一组多余的括号,以强调eval使用一个解析器并返回一个函数的事实。
由于GADT的结构,每个递归调用都可以使用不同类型的解析器来调用该函数;因此,我们需要对多态局部抽象类型使用语法。以下是一个完整的实现。对于选项类型,它假定适用于应用程序和Monadic组合器的模块为Option.Syntax,如下所述:
让我们回想一下:键入a。 t->字符列表-> (a *字符列表)选项= fun p->将p与|匹配失败_-> const无|空->有趣的CS-> (将CS与| []-> Some((),[])| _-> None匹配)|返回x->有趣的CS->一些(x,cs)|符号s->有趣的CS-> (当c = s-> Some(s,cs)| _-> None时,将cs与| c :: cs匹配)|映射(f,p)->令ep = eval p有趣的CS->选项 。语法(let +(x,cs)= ep cs in(f x,cs))|乘积(p,q)->令ep = eval p,令eq = eval q的乐趣cs->选项 。语法(let *(x,cs)= ep cs in let *(y,cs)= eq cs in Some((x,y),cs))| (p,q)->令ep = eval p,令eq = eval q的乐趣cs-> (用| Some r-> Some r | None-> eq cs匹配ep cs)|修复f->有趣的CS->评估(f(Fix f))cs
模式匹配的每个分支都返回一个接受字符列表并产生可选结果的函数。
为方便起见,我们可以将其公开为一个函数,该函数将解析器规范转换为使用字符串的函数以实际执行解析:
#eval'浮动&#34.12.34" ;; -:(float * char list)选项=某些(12. 34,[])#eval'浮动" 1.2e3" ;; -:(float * char list)选项=某些(1200。,[])#eval'浮动" a1.23" ;; -:(float * char list)选项=无
eval的实现不是回溯,而是仅针对给定的输入返回解析器的一个可能匹配项。一种替代实现将返回结果列表,如下所示:
符号提取是解析器解释器的另一个示例-通过遍历其树来识别解析器所有可能输入的集合的功能:
模块CS =设置。 Make(Char)let符号p = let recaux:键入a。 t-> CS 。 t =函数|失败_-> CS 。空的空-> CS 。空的返回_-> CS 。空的符号c-> CS 。单例c |地图(_,p)->辅助p |乘积(p,q)-> CS 。联合(辅助p)(辅助q)| (p,q)-> CS 。联合(辅助p)(辅助q)|修复f-> aux @@ f @@在aux p |>中修正(fun _->失败"")。 CS 。 to_seq |>列表 。 of_seq
大多数情况都是微不足道的-例如,Symbol c返回带有c的单例集,而Either返回每个分支中符号的并集。Fix f可能是最有趣的情况。 Fix的有效负载具有以下类型的功能: < t,因此我们只需要传递一些解析器,该解析器不会引入任何其他符号,因此会失败。
#个符号浮动;; - : CS 。 elt list = ['。' ; ' 0' ; ' 1' ; ' 2' ; ' 3' ; ' 4' ; ' 5' ; ' 6' ; ' 7' ; ' 8' ; ' 9' ; ' e' ]
作为解析器解释器的最后一个示例,我们将编写一个将解析器转换为字符串的函数:
让rec显示:输入a。 t->字符串=函数|信息失败->味精|空-> "空" |符号c-> Printf。 sprintf"符号'%c'" c |返回x-> "返回?" |映射(f,p)-> Printf。 sprintf" map? (%s)" (显示p)|乘积(p,q)-> Printf。 sprintf"产品(%s)(%s)" (显示p)(显示q)| (p,q)-> Printf。 sprintf"任一(%s)(%s)" (显示p)(显示q)|修复f-> Printf。 sprintf"让f()中的rec f()=%s) @@ show(f(失败" f()"))
请注意,我们无法完全显示所有结构。 Map的有效负载包含一个我们不知道如何打印的功能。我们还缺少打印机以返回参数。对于完全透明的解析器,我们必须强加一些其他结构(例如,将参数与打印机配对),但是现在,我们仅插入一些问号以指示不透明的参数。还要注意我们如何恢复Fix f的递归结构,方法是像let rec ..那样打印它。例如,考虑打印由许多组合器解析的解析器:
#let p = many(符号' a');; val p:字符列表P。 t =固定<乐趣> #print_endline @@ show p ;; let rec f()=在f()中的(map?(product(map?(symbol' a'))(f())))(return())
要了解这有何意义,请在下面的版本中填写问号:
let rec f()=或者(map List。cons(product(map id(symbol' a'))(f())))(return(])in f()
这与我们在引入定点组合器之前开始的许多递归版本非常相似。
在查看eval的实现时,您可能已经注意到,在每种情况下(除了Fix f之外),返回函数的主体中都没有递归调用。例如,在乘积(p,q)的情况下,在返回使用实际输入的函数之前,先对p和q进行求值。因此,对于不使用fix的解析器–解析器AST的所有痕迹都将被eval消除,并且在实际解析过程中我们无需支付任何额外的运行时成本。
是否可以修改Fix f的情况以提前进行评估?第一个尝试是简单地排除递归调用:
|修复f->让g = eval @@ f(修复f)有趣的cs-> g cs
但是,人们很快意识到,由于评估可能会停留在不断扩展的f(Fix f)表达式上,因此并不能完全解决问题。请记住,引入了修订程序来编码许多组合器所需的递归定义。该策略是在评估级别将其转换回递归定义,类似于我们在上面的show实施中为打印所做的操作。
aux主体是通过将函数f应用于可能会回调自身的其他表达式来给出的。但是,问题在于f的参数本身必须是解析器表达式。我们可以通过引入另一个节点来保留原始评估程序来解决此问题:
输入' a t = ... | Raw:(字符列表->(' a *字符列表)选项)-> '在
暴露这样的构造函数当然会挫败首先使用定点组合器的目的,因为无论如何我们都必须放弃符号提取和打印。但是,我们仅将其用作内部帮助程序节点。如果我们专门导出智能构造函数,并使其类型为抽象或私有,则用户将永远无法构造原始节点。因此,对于其他任何评估者,我们可以放心地忽略这种情况。
返回到eval的实现,这里介绍了递归转换的实现方式:
让我们回想一下:键入a。 t->字符列表-> (a *字符列表)选项= fun p-> ... |修复f-> let rec k =懒惰(eval @@ f(Raw(fun cs-> Lazy。force k cs)))。力k |原始f-> F
k的递归定义懒惰地调用eval。 但是,由于我们立即对其进行强制,因此我们只对其进行过一次评估! 通过应用f返回的表达式可以包含一个子节点Raw g,其中g表示k。另一种选择是使用ref-cells而不是lazy构造。 在查看如何使用解析库的另一个示例之前,有一些实用程序函数会很方便: val one_of:字符串-> 之间的字符:' t-> ' b t-> ' c t-> ' 忘记:' t-> 在 在p1 p2 p之间的是一个解析器,该解析器首先解析p1,然后是p,然后是p2,并且仅返回p的结果。 函数one_of和between之间可以用现有的组合器来编写。 至于 ......