最近我一直在编写一个简单的编译器来更好地理解编译器概念 . 作为stackoverfolow的勤奋读者,似乎有一种共识,即在函数式语言中编写编译器比命令式编写器更容易 . 为此,我想我会尝试杀死两只鸟,并在F#中编写一个编译器来学习一种函数式语言并同时编写一个编译器 .
我一直在阅读龙书,并决定从F#手写的递归下降解析器开始 . 然而,龙书几乎所有的代码样本都是命令式的 . 例如,匹配令牌功能通过副作用完成其工作的重要部分 .
所以我的问题是什么是更传统的解析功能方法(即几个副作用)看起来像什么?我知道Haskell编译器(GHC)是用Haskell编写的,但我希望能够理解一些更小,更容易理解的代码示例 .
第二,尝试采用功能性方法进行解析是否值得,或者它是否真的在对功能语言闪耀的中间代码进行优化,而我还没有到达那里?也就是说,我是否应该使用命令式样式在F#中进行解析并稍后切换到更具功能性的方法?
5 回答
来自this blog article的答案:
听起来你需要将功能(如Lisp,Scheme,Standard ML,CAML,OCaml,F#)与纯度(没有副作用,如Haskell)和偶然语言特征(代数数据类型,模式匹配)分开 .
由于代数数据类型,模式匹配和高阶函数,F#非常适合解析,非常适合转换和代码生成,但大多数用F#编写的生成解析器都不是纯粹的 . 从历史上看,F#语言系列主要源自(MetaLanguages或MLs),专门用于这种元编程 .
这是一组非常简单的相互递归的活动模式,它解析和评估由单个数字,
+ - *
运算符和括号子表达式组成的数学表达式:以下是用于解析和计算表达式的示例:
这是一个纯粹的解决方案,它使用带有F#活动模式的列表进行模式匹配 . 实际上,您需要为抽象语法树定义一个类型并返回该类型的值 . 这在F#中非常简单:
请注意,只需要对解析器进行一次小的更改,因为AST也可以使用
+
,-
和*
运算符构造 .你说的是纯度,而不是函数式编程 . 纯度在解析文本的上下文中并不特别有用,事实上,它可能是一个真正的障碍(例如,实习符号在Haskell中是一场噩梦) . 但是,F#还有许多其他好处,可以解决这一系列问题 . 特别是,尽管像OCaml这样的其他语言有更好的解析工具,但我认为F#在这种情况下是最好的.NET语言 .
完全取决于你想要的功能 . 我会使用fslex和fsyacc与纯代码在动作中构建AST,但是对于像散列消耗或生成唯一ID这样的杂质 .
您可能会对我在this blog(注意付费专区)上就此主题撰写的以下文章表示感谢:
"Parsing text with Lex and Yacc"(2007年9月30日) .
"Optimizing a simple bytecode interpreter"(2007年10月31日) .
"Parser combinators"(2007年11月30日) .
"Language-oriented programming: The Term-level Interpreter"(2007年12月31日) .
"Language-oriented programming: Term Rewriting"(2008年8月16日) .
“使用
System.Reflection.Emit
生成运行时代码”(2008年8月31日) ."Parsing and visualizing binary Geographic Information System data"(2009年11月30日) .
功能解析的一种策略是monadic解析器组合器 . 你可以阅读一些关于它here(并按照链接)或使用像FParsec这样的库 . 但是,如果您只是学习/启动F#/编译器,我不推荐这种方法 .
使用F#的另一种方法是使用FsLex / FsYacc(在PowerPack中) . 我有点厌恶Lex / Yacc技术,所以我也不推荐这个 .
我认为你应该手工编写一个递归的正确解析器 . 我对标记化器没有强烈的感觉,只是简单地将整个文件标记为一个(n个不可变的)标记,然后进行递归下降(并利用一些模式匹配)是处理解析的好方法 . 当然,您将需要使用已取消的联合来表示解析器的AST输出(la here) .
我很久没看过龙书,但我显然是这个星球上唯一一个不喜欢它的人 . 我会考虑放弃该文本,转而讨论使用基于ML的语言讨论编译器的书,尽管我不能推荐一个 .
编辑
我有一段时间没有做过其中的一个,所以我花了几分钟来编写一个小样本 .
(希望没有令人震惊的错误 . )
Parser组合器确实很漂亮! FParsec是一个非常光滑的monadic解析器组合库,你应该检查 . 如果你想从一些简单但仍然纯粹功能的东西开始,你可能会喜欢F#系列中的Scheme解释器中的tokenizer / parser(我的博客):http://blogs.msdn.com/b/ashleyf/archive/2010/09/24/fscheme-0-0-0.aspx
比其他好答案更简单的答案:
函数语言中的解析器将令牌流带入解析树和令牌流的其余部分 . 也就是说,它有类型
递归的正确解析器通常具有大量此类型的函数,这些函数会占用令牌流,然后构建解析树的一小部分 . 通过递归调用这些(递归正常) - 你得到你想要的 .
下一步是使用更高阶的解析器:在其他解析器上运行的解析器 . 这就是解析器组合库所做的事情 . 也许您可以从简单的递归方案开始,然后将其升级到解析器组合器 .
我已经在F#中使用ECMAScript编译器了一段时间,所以我和你在同一条船上 . Mayhap我的一些工作可能对你有用 . 这是一个简单的解析器组合库,我一直在研究它与FParsec结合使用 . 它不是近乎完美的地方,但它应该给你一些简单的东西来学习,这样你就可以转向更高级的东西 . 如果你最终使用FParsec,你可能会注意到这里的很多东西都是受它启发的 .
NOTE:
LazyList
类型需要FSharp PowerPack .Example: