在Haskell中编写Haskell解释器

90

一个经典的编程练习是用Lisp / Scheme写一个Lisp / Scheme解释器。可以利用完整语言的强大功能来生成语言子集的解释器。

那么Haskell有没有类似的练习呢?我想使用Haskell作为引擎来实现Haskell的子集。当然这是可行的,但是否有任何在线资源可供参考呢?


以下是背景故事。

我正在探索使用Haskell作为一门离散结构课程中探索一些概念的语言。对于这个学期,我已经选择了Miranda,这是一种更小的语言,启发了Haskell。Miranda可以完成我想要做的90%,但Haskell可以完成2000%:)

因此,我的想法是创建一种具有恰好我想要的Haskell特性并禁止其他所有内容的语言。随着学生的进步,他们可以逐渐地“打开”各种特性,一旦他们掌握了基础知识。

已成功使用教学“语言级别”来教授JavaScheme。通过限制他们的操作范围,可以在他们掌握正在教授的语法和概念时防止他们犯错。此外,还可以提供更好的错误消息。


1
我已经使用 Typing Haskell in Haskell 作为基础实现了一个 WIP Haskell 方言。这里有一个演示:http://chrisdone.com/toys/duet-delta/。它还没有准备好公开发布,但如果您感兴趣,我可以与您分享源代码。 - Christopher Done
15个回答

77

我喜欢你的目标,但这是一项艰巨的工作。 这里有几个提示:

  • 我曾经在GHC上工作过,你不想接触源代码。Hugs是一个更简单、更干净的实现,但不幸的是它是用C写的。

  • 这只是问题的一个小部分,但Mark Jones写了一篇美妙的文章,名为Typing Haskell in Haskell,这将是你前端优秀起点。

祝你好运!从课堂支持证据看,识别Haskell语言级别将对社区有很大帮助,并且绝对是可发表的结果!


3
我想知道关于GHC的评论是否仍然准确。 GHC是复杂的,但它有相当完善的文档。特别是内部的“Notes”对于理解低级细节很有帮助,而《开源应用程序架构》中关于GHC的章节提供了优秀的高层次概述。 - sjy

39

有一个完整的Haskell解析器:http://hackage.haskell.org/package/haskell-src-exts

一旦你解析了它,剥离或禁止某些内容就很容易了。我在tryhaskell.org上就这样做,以禁止导入语句,支持顶层定义等。

只需解析模块:

parseModule :: String -> ParseResult Module

然后,您将获得一个模块的AST:
Module SrcLoc ModuleName [ModulePragma] (Maybe WarningText) (Maybe [ExportSpec]) [ImportDecl] [Decl]    

Decl类型非常广泛:http://hackage.haskell.org/packages/archive/haskell-src-exts/1.9.0/doc/html/Language-Haskell-Exts-Syntax.html#t%3ADecl
你只需要定义一个白名单,包含可用的声明、导入、符号和语法,然后遍历AST,在任何你不想让他们知道的地方抛出“解析错误”。你可以使用AST中每个节点附加的SrcLoc值。
data SrcLoc = SrcLoc
     { srcFilename :: String
     , srcLine :: Int
     , srcColumn :: Int
     }

不需要重新实现Haskell。如果想提供更友好的编译错误提示,只需解析代码、过滤它、发送到编译器并解析编译器输出即可。如果出现“couldn't match expected type a against inferred a -> b”,那么很可能是函数参数过少。

除非您真的想花时间从头开始实现Haskell或者搞乱Hugs的内部,或者某些愚蠢的实现,否则我认为您应该只过滤传递给GHC的内容。这样,如果您的学生想要将他们的代码库带到下一个级别并编写一些真正完整的Haskell代码,过渡是透明的。

24

您想从零开始构建自己的解释器吗?首先实现一种更容易的函数式语言,比如 lambda 演算或 Lisp 变种。对于后者,有一个很好的维基书籍叫做在 48 小时内写一个 Scheme,介绍了酷炫而实用的解析和解释技术。

手工解释 Haskell 的复杂性将会更高,因为你需要处理高度复杂的特性,如 typeclasses、极其强大的类型系统(类型推导!)以及惰性求值(化简技术)。

所以,您应该定义一个相当小的 Haskell 子集来使用,然后也许可以逐步扩展 Scheme 示例。

补充说明:

请注意,在 Haskell 中,您可以完全访问解释器的 API(至少在 GHC 下),包括解析器、编译器和当然解释器。

要使用的程序包是hint (Language.Haskell.*)。可惜我既没有找到在线教程,也没有自己试过,但它看起来很有前途。


12
请注意,类型推断实际上是一种非常简单的算法,只需要20-30行代码就可以实现。它的简洁之美令人惊叹。惰性求值也不难编码。我会说难点在于疯狂的语法、模式匹配以及语言中大量的东西。 - Claudiu
有趣 - 你能发布类型推断算法的链接吗? - Dario
5
好的,这是免费电子书的链接 - http://www.cs.brown.edu/~sk/Publications/Books/ProgLangs/2007-04-26/ - ,它在第273页 (PDF中的第289页)。算法伪代码在第296页。 - Claudiu
1
在《函数式编程语言的实现》一书中,还有一个类型推断/检查算法的实现。 - Phil Armstrong
1
类型类的类型推断并不简单。 - Christopher Done

20
我建议一个更简单(即涉及的工作更少)的解决方案。不要创建一个可以关闭功能的Haskell实现,而是用一个程序包装一个Haskell编译器,该程序首先检查代码是否使用了您禁止使用的任何功能,然后使用现成的编译器进行编译。
这类似于HLint(也有点相反):
HLint(前身为Dr. Haskell)读取Haskell程序并建议更改,以便使它们更易于阅读。 HLint还可以轻松禁用不需要的建议,并添加自定义建议。
实施自己的HLint“建议”,以不使用您不允许的功能
禁用所有标准的HLint建议。
使您的包装器将修改后的HLint作为第一步运行
将HLint建议视为错误。 也就是说,如果HLint“抱怨”,则程序不会进入编译阶段

16

7
如果你想要一个易于实现的Haskell子集,可以放弃类型类和类型检查。没有类型类,你不需要类型推断来评估Haskell代码。
我为Code Golf挑战编写了一个自编译的Haskell子集编译器。它接受Haskell子集代码作为输入,并在输出上产生C代码。很抱歉没有更可读的版本可用;在制作自编译过程中,我手工提取了嵌套定义。
对于一个有兴趣实现Haskell子集解释器的学生,我建议从以下功能开始:
  • 惰性求值。 如果解释器是Haskell,则您可能不需要做任何事情。

  • 带有模式匹配参数和保护的函数定义。 仅关注变量,cons,nil和_模式。

  • 简单的表达式语法:

    • 整数字面量

    • 字符字面量

    • [](nil)

    • 函数应用(从左到右结合)

    • 中缀:(cons,从右到左结合)

    • 括号

    • 变量名

    • 函数名称

更具体地说,编写一个可以运行以下内容的解释器:

-- tail :: [a] -> [a]
tail (_:xs) = xs

-- append :: [a] -> [a] -> [a]
append []     ys = ys
append (x:xs) ys = x : append xs ys

-- zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:as) (b:bs) = f a b : zipWith f as bs
zipWith _ _      _      = []

-- showList :: (a -> String) -> [a] -> String
showList _    []     = '[' : ']' : []
showList show (x:xs) = '[' : append (show x) (showItems show xs)

-- showItems :: (a -> String) -> [a] -> String
showItems show []     = ']' : []
showItems show (x:xs) = ',' : append (show x) (showItems show xs)

-- fibs :: [Int]
fibs = 0 : 1 : zipWith add fibs (tail fibs)

-- main :: String
main = showList showInt (take 40 fibs)

类型检查是Haskell的一个关键特性。然而,从无到有地编写一个能够进行类型检查的Haskell编译器非常困难。如果您首先编写上述内容的解释器,那么为它添加类型检查应该会更容易些。

惰性求值。如果解释器是Haskell,你可能不需要为此做任何事情。这可能并不总是正确的。有关在Haskell中实现惰性解释器的更多信息,请参阅Naylor在http://www.haskell.org/wikiupload/0/0a/TMR-Issue10.pdf中的文章。 - Jared Updike

6
EHC系列编译器可能是最好的选择:它在积极开发中,看起来正是你想要的——一系列小型λ演算编译器/解释器,最终实现Haskell'98。但你也可以看看Pierce的《类型与编程语言》中开发的各种语言,或者Helium解释器(一个面向学生的被削弱的Haskell,详情请见http://en.wikipedia.org/wiki/Helium_(Haskell))。

3
这可能是个好主意——用Haskell制作一个NetLogo的微型版本。这里有一个微型解释器。请点击此处此处查看详情。

这些链接已经失效了。这个内容还有可能在其他地方存在吗?我很好奇... - Nicolas Payette
哦,这是一篇博客文章,我不知道用什么关键词来搜索它。提供链接时包含更多实质信息的好教训... - Claudiu
1
谷歌搜索“netlogo haskell”会出现...这个问题。不管怎样,没什么大不了的。谢谢! - Nicolas Payette

3

你可以看看Happy(Haskell中类似yacc的解析器),它有一个Haskell解析器。


2

安德烈·鲍尔(Andrej Bauer)的编程语言动物园有一个名为"minihaskell"的小型纯函数式编程语言实现,非常机智。它只有大约700行OCaml代码,因此非常易于理解。

该网站还包含了ML风格、Prolog风格和面向对象编程语言的玩具版本。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接