Haskell, Lisp, and verbosity
对于那些同时体验过Haskell和Lisp的人,我很好奇用Haskell和Lisp编写代码是多么"愉快"(用一个可怕的术语)。
一些背景:我现在正在学习haskell,之前曾与scheme和cl(以及对clojure的一点探索)合作过。传统上,您可以认为我是动态语言的爱好者,因为它们提供的简洁和快速。我很快就爱上了lisp宏,因为它给了我另一种避免冗长和样板文件的方法。
我发现Haskell非常有趣,因为它向我介绍了我不知道存在的编码方式。它肯定有一些方面看起来有助于实现敏捷性,比如部分函数的编写容易。但是,我有点担心丢失Lisp宏(我假设我丢失了它们;事实上我可能还没有了解它们?)以及静态打字系统。
在两个世界中都做了大量编码的人会不会对体验的差异、你喜欢哪种以及如果所说的偏好是情境性的进行评论?
简短回答:
- 几乎任何你能用宏做的事情,你都可以用一个高阶函数(我包括单子,箭头等),但它可能需要更多的思考(但只是第一次,这很有趣,你会成为一个更好的程序员),以及
- 静态系统是非常通用的,它永远不会妨碍您的方式,而且有点令人惊讶的是,它实际上"有助于实现敏捷性"(如您所说),因为当您的程序编译时,您几乎可以确定这是正确的,因此这种确定性让您尝试一些您可能害怕尝试的东西——编程时有一种"动态"的感觉。虽然这和Lisp不一样。
[注意:有一个"template haskell"允许您像在Lisp中一样编写宏,但严格来说,您不应该需要它。]
- 来自Conor McBride,Don Stewart引用:"我喜欢把类型看作是扭曲我们的重力,这样我们需要走的方向(写正确的程序)就变成了"下坡路"。"类型系统使得写正确的程序出奇地容易……看这篇文章和它的重新分享。
- 高阶函数不能替换宏,事实上,cl由于某种原因两者都有。CL中宏的真正强大之处在于,它们允许开发人员引入新的语言特性,帮助更好地表达问题的解决方案,而不必等待像Haskell或Java那样的新版本的语言。例如,如果haskell拥有这种能力,那么haskell的作者就不需要编写GHC扩展,开发人员是否可以随时将它们作为宏来实现。
- @MLJRG你有一个具体的例子吗?请参阅下面对hibou57答案的评论,其中一个所谓的例子被证明是可疑的。我很想知道你的意思(例如haskell代码有宏和没有宏)。
- 从哈斯克尔那里取出咖喱。你能用哈斯克尔剩下的东西来实现它吗?另一个例子:假设haskell不支持模式匹配,您可以自己添加它而不必让ghc的开发人员支持它吗?在cl中,可以使用宏任意扩展语言。我想这就是为什么cl语言自90年代标准化以来没有发生变化,而haskell在ghc中似乎有一个永无止境的扩展流。
首先,不要担心丢失诸如动态输入之类的特殊功能。正如您熟悉公共Lisp(一种设计非常好的语言)一样,我假设您知道一种语言不能简化为它的特性集。这是一个连贯的整体,不是吗?
在这方面,哈斯克尔的光芒和普通的口齿不清一模一样。它的特性结合起来为您提供了一种编程方法,使代码非常简短和优雅。宏的缺乏在某种程度上可以通过更复杂的概念(同样,更难理解和使用)来缓解,比如monads和arrows。静态类型系统增加了您的能力,而不是像大多数面向对象语言那样阻碍您的工作。
另一方面,haskell中的编程与lisp的交互作用要小得多,而且像lisp这样的语言中存在的大量反射并不符合haskell预先假定的静态世界视图。因此,您可以使用的工具集在这两种语言之间是非常不同的,但很难相互比较。
我个人更喜欢Lisp的编程方法,因为我觉得它更适合我的工作方式。然而,这并不意味着你也一定要这么做。
- 你能详细阐述一下"Haskell中的编程没有那么多的互动性"吗?GHCI不是真的提供了你所需要的一切吗?
- @Johannesgerer:我没有尝试过,但据我所知,ghci并不是运行映像中的shell,您可以在运行时重新定义和扩展整个程序的任意部分。此外,haskell语法使以编程方式在repl和编辑器之间复制程序片段变得更加困难。
与普通的Lisp相比,haskell对元编程的需求更少,因为许多元编程可以围绕monads进行,并且添加的语法使嵌入的DSL看起来不像树,但是始终有模板haskell,正如shrevatsar所提到的,甚至Liskell(haskell语义+Lisp语法),如果您喜欢括号的话。
关于宏,这里有一个关于它的页面:你好,哈斯克尔,再见,Lisp。它解释了哈斯克尔不需要宏的观点。它附带了一个简短的比较示例。
例如,需要lisp宏来避免对两个参数进行计算:
1
| (defmacro doif (x y) `(if ,x ,y)) |
例如,如果haskell没有系统地评估这两个参数,而不需要宏定义:
1
| doif x y = if x then (Just y) else Nothing |
和Voice
- 这是一个常见的误解。是的,在haskell中,懒惰意味着当您想要避免计算表达式的某些部分时,不需要宏,但这些只是所有宏使用中最微不足道的子集。谷歌为"猪在Perl之前"做了一次演讲,展示了一个不能用懒惰来完成的宏。另外,如果您希望某些位严格,那么您不能将其作为一个函数来执行——这反映了这样一个事实:方案的delay不能是一个函数。
- @我觉得这个例子不太有说服力。下面是幻灯片40的完整、简单的haskell翻译:pastebin.com/8rfywtre
- @里德·巴顿:嗯?本文的重点是创建一个宏,它实际上是一个小的DSL,用于指定要"编译"到方案代码的自动化程序。您的代码Otoh是代码的一种简单翻译,但是(a)它使用了Shriram在开始时提到的表查找,更重要的是,(b)您使用的是纯Haskell,结果仍然不接近定义这样的DSL。事实上,这证明了"编写这样的代码很容易,在表中使用函数值时也更容易"。也就是说,与宏无关。
- @伊莱:我完全不理解你的反应。accept是(e)DSL。accept函数类似于前几页概述的宏,v的定义与幻灯片40方案中v的定义完全平行。Haskell和Scheme函数使用相同的评估策略计算相同的内容。最多,宏允许您向优化器公开程序的更多结构。你很难声称这是一个宏以一种不被懒惰的计算所复制的方式增加语言表达能力的例子。
- 我不会跟踪这些。首先,是的——accept是完成工作的函数,但它不是DSL,它和所有其他函数一样是一个函数——比如在所有子列表中使用它,或者需要使用where及其自身范围,这正是宏不必要的。至于懒惰的评估——你没有以任何重要的方式使用它,所以我看不出整个论点是如何相关的。
- @eli barzilay:在一个假设的懒惰方案中,你可以这样写:pastebin.com/tn3f8vve我的一般主张是,这个宏几乎不会给你带来什么:稍微不同的语法和优化器更容易的时间(但"足够聪明的编译器"并不重要)。作为交换,你陷入了一种无法表达的语言中;你如何定义一个匹配任何字母而不列出所有字母的自动机?另外,我不知道你所说的"在所有子列表中使用它"或"在其自身范围内需要使用where"是什么意思。
- 里德:(a)一个懒惰的计划不是假设性的——一个计划已经被敲诈了几年;(b)宏仍然有用的事实有一个很好的提示;(c)你写的东西也显示了为什么宏有用——它不使用宏,因此不是施莱姆所说的DSL;(d)通过"在子列表中使用它"等我的意思是你对实施中的"DSL"有一定的要求(例如,使用accept)——这就是它不是DSL的原因之一;
- (e)如果您考虑将严格的运算符添加到懒惰的语言中,那么认为懒惰会使流控制宏(没有新绑定的宏)冗余的假象是伪造的——使用这样的运算符也需要特殊的窗体(和宏);(f)另一点:如果Haskell中不需要宏,那么它怎么会有这些宏呢?(甚至在月日之前,也有人使用CPP。)
- 最后,(g)你肯定是被困在了一种"无表达的语言"中——关键是DSL——而不是GPL。显然,可以编写一个更复杂的宏,该宏将具有方案表达式(就像是一个符号的谓词,而不是全部列出),但在本例中,这超出了DSL的范围。
- 好吧,我放弃。显然,您对DSL的定义是"宏的论据",因此,我的懒惰方案示例不是DSL,尽管它在语法上与原始方案是同构的(automaton变为letrec,:变为accept,->在这个版本中变为无)。无论什么。
- 这篇文章中的链接似乎在hibou57上被破坏了,你能在必要时检查并修复你的文章吗?
- @Batbrat,已修复并感谢您将此问题通知我(他们更改了此网站上的URL布局)。
- @Hibou57,谢谢!:)
我是一个普通的Lisp程序员。
一段时间前尝试过哈斯克尔,我的个人底线是坚持使用cl。
原因:
- 动态类型(查看动态与静态类型-基于模式的分析帕斯卡·科斯坦扎)
- 可选参数和关键字参数
- 带有宏的统一同形列表语法
- 前缀语法(不需要记住优先规则)
- 不纯,因此更适合快速成型
- 具有元对象协议的强大对象系统
- 成熟标准
- 各种编译器
当然,哈斯克尔也有其自身的优点,并且以一种根本不同的方式做了一些事情,但对我来说,这并不是长期的。
- 嘿,你碰巧有你链接的Costanza报纸的标题吗?似乎该文件已被移动。
- 可能是这个:p-cos.net/documents/dynatype.pdf
- 注意haskell也支持前缀语法,但是我认为monad>>=使用它会非常难看。我也不同意不洁是一种祝福:p
- 我喜欢这个旁注:我们还没有收集到经验数据,这个问题是否会在现实世界的程序中引起严重的问题。
- 本文中的所有例子(pascal costanza,动态与静态类型——基于模式的分析)都不适用于haskell。它们都是Java特有的(或者更确切地说,是面向"面向对象的编程"),我看不到Haskell提出的任何问题。类似地,所有其他的论点都是有争议的:可以说haskell是"纯粹的,因此更适合快速原型化",前缀语法不是强制性的,它没有执行不同操作的广泛编译器等等。
- 那篇论文实际上几乎完全与哈斯克尔无关。"EDOCX1?1"?"?我怀疑许多Haskell程序员甚至可以在不动一点的情况下阅读这篇文章。
在Haskell中,可以定义if函数,这在Lisp中是不可能的。这是可能的,因为懒惰,它允许在程序中实现更多的模块化。这篇经典的论文:为什么FP很重要由约翰休斯,解释了如何懒惰提高可组合性。
- Scheme(两种主要的Lisp方言之一)实际上有懒惰的评估,尽管它并不像Haskell那样默认。
- (defmacro doif(x y)`(if,x,y))
- 宏与函数不同——例如,宏不能很好地处理像fold这样的高阶函数,而非严格函数则可以。
使用Haskell中的宏(如果可能的话)可以在Lisp中实现一些非常酷的功能。以"memoize"宏为例(参见彼得·诺维格的paip第9章)。使用它,您可以定义一个函数,比如foo,然后简单地评估(memoize'foo),它将foo的全局定义替换为memoize版本。你能在具有高阶函数的haskell中达到同样的效果吗?
- 不完全是这样(afaik),但是您可以通过修改函数(假定它是递归的)来做类似的事情,将函数作为参数(!)递归调用。而不是简单地称呼自己:haskell.org/haskellwiki/memoization
- 您可以将foo添加到一个惰性数据结构中,在该结构中,值将在计算后存储。这实际上是相同的。
当我继续我的haskell学习之旅时,似乎有一件事可以帮助"替换"宏,那就是定义自己的中缀运算符并自定义它们的优先级和关联性。有点复杂,但系统很有趣!