Haskell有什么大惊小怪的?[关闭]


109

我认识一些程序员,他们在一起时总是在谈论Haskell,因此在这里每个人似乎都喜欢这种语言。擅长Haskell似乎有点天才程序员的特点。

有人可以举一些Haskell的例子来说明为什么它如此优雅/优越吗?

Answers:


134

它向我介绍的方式,以及我在Haskell上学习一个月后的真实想法,是函数式编程以有趣的方式扭曲了您的大脑:它迫使您以不同的方式思考熟悉的问题:而不是循环,请考虑地图,折叠和过滤器等。通常,如果您对一个问题有多个观点,则可以使您更好地推理此问题,并根据需要切换观点。

Haskell的另一个真正的优点是它的类型系统。它是严格类型化的,但是类型推断引擎使它看起来像一个Python程序,可以神奇地告诉您何时犯了与类型相关的愚蠢错误。Haskell在这方面的错误消息有所欠缺,但是随着您对语言的熟悉,您会对自己说:这就是键入应该是的!


47
应该注意的是,并不缺少Haskell的错误消息,而ghc则没有。Haskell标准未指定错误消息的处理方式。
PyRulez 2014年

对于像我这样的群众,GHC代表格拉斯哥Haskell编译器。en.wikipedia.org/wiki/Glasgow_Haskell_Compiler
Lorem Ipsum

137

这是说服我学习Haskell 示例(男孩很高兴我做到了)。

-- program to copy a file --
import System.Environment

main = do
         --read command-line arguments
         [file1, file2] <- getArgs

         --copy file contents
         str <- readFile file1
         writeFile file2 str

好的,这是一个简短易读的程序。从这个意义上讲,它比C程序更好。但是,这与(例如)结构非常相似的Python程序有何不同?

答案是懒惰的评估。在大多数语言(甚至是某些功能语言)中,如上一种结构的程序会导致将整个文件加载到内存中,然后以新名称再次写出。

Haskell是“懒惰的”。直到需要时,它才计算内容,并且通过扩展,它也不再计算它不需要的内容。例如,如果要删除该writeFile行,Haskell首先不会费心从文件中读取任何内容。

实际上,Haskell意识到writeFile取决于readFile,因此能够优化此数据路径。

虽然结果取决于编译器,但运行上述程序时通常会发生以下情况:该程序读取第一个文件的一个块(例如8KB),然后将其写入第二个文件,然后从第一个文件读取另一个块文件,并将其写入第二个文件,依此类推。(尝试运行strace!)

...看起来很像文件副本的高效C实现。

因此,Haskell使您可以编写紧凑,易读的程序-通常不会牺牲很多性能。

我必须补充的另一件事是,Haskell使得编写错误的程序变得很困难。令人惊叹的类型系统,没有副作用,当然Haskell代码的紧凑性至少在三个方面减少了错误:

  1. 更好的程序设计。降低的复杂度导致更少的逻辑错误。

  2. 紧凑的代码。出现错误的行减少了。

  3. 编译错误。许多错误只是无效的Haskell

Haskell并非适合所有人。但是每个人都应该尝试一下。


您将如何精确地更改8KB常数(或其他常数)?因为我敢打赌Haskell的实现会比C版本慢,否则,尤其是在没有预取的情况下……
user541686 2015年

1
@Mehrdad您可以使用更改缓冲区大小hSetBuffering handle (BlockBuffering (Just bufferSize))
大卫

3
令人惊讶的是,这个答案有116票赞成票,但里面有错。该程序读取整个文件,除非您使用惰性字节串(可以使用Data.Bytestring.Lazy.readFile),而这与Haskell是一种惰性(非严格)语言无关。Monad正在按顺序排列-大致意味着“取出结果时,所有副作用都已完成”。至于“惰性字节串”魔术:这很危险,您可以使用大多数其他语言中的相似或更简单的语法来做到这一点。
乔苏

14
无聊的旧标准readFile也以相同的方式Data.ByteString.Lazy.readFile执行惰性IO 。因此,答案是正确的,不仅是编译器优化。的确,这是Haskell规范的一部分:“该readFile函数读取文件并以字符串形式返回文件的内容。该文件按需延迟读取,就像使用getContents。一样。”
丹尼尔·瓦格纳

1
我认为其他答案指向有关Haskell的更特别的事情。许多语言/环境都有流,您可以在Node:中执行类似的操作const fs = require('fs'); const [file1, file2] = process.argv.slice(2); fs.createReadStream(file1).pipe(fs.createWriteStream(file2))。Bash也有类似的东西:cat $1 > $2
Max Heiber

64

您在问一个错误的问题。

Haskell不是一门语言,您可以看一些很酷的例子然后说:“啊哈,我现在知道了,就是使它变得更好的原因!”

这更像是,我们拥有所有其他所有编程语言,它们或多或少都相似,然后就是Haskell,它完全不同且古怪,一旦您习惯了古怪性就完全棒了。但是问题是,要花很长时间才能适应这种古怪性。使Haskell与几乎任何其他偶半主流语言区分开的事情:

  • 懒惰评估
  • 无副作用(一切都是纯净的,IO /等通过单子发生)
  • 表现力超强的静态类型系统

以及与许多主流语言不同(但某些语言共有)的其他一些方面:

  • 功能性
  • 有效空白
  • 类型推断

正如其他海报所回答的那样,所有这些功能的组合意味着您以完全不同的方式考虑编程。因此很难想出一个(或一组)示例来充分地将此信息传达给Joe-mainstream-programmer。这是一个体验性的事情。(举个比喻,我可以给你看我1970年中国之行的照片,但是看到这些照片后,您仍然不知道那段时间住在那儿的感觉如何。类似地,我可以给你看一个Haskell “快速排序”,但您仍然不知道成为Haskeller意味着什么。)


17
我不同意你的第一句话。最初的一些Haskell代码示例给我留下了深刻的印象,使我真正值得学习的是这篇文章:cs.dartmouth.edu/~doug/powser.html 但是,当然,这对数学家/物理学家来说很有趣。程序员研究现实世界的东西会发现这个例子很荒谬。
拉斐尔·S·卡尔萨维里尼(Lafael S. Calsaverini)2009年

2
@Rafael:这就引出了一个问题:“对现实世界中的程序员有什么印象”?
JD

好问题!我不是“现实世界”程序员,所以我不知道他们喜欢什么。哈哈哈...我知道物理学家和数学家喜欢什么。:P
拉斐尔·卡尔萨维里尼

27

真正使Haskell与众不同的是它在设计中为执行功能性编程而付出的努力。您可以使用几乎任何语言编写功能性风格的程序,但是要在第一时间放弃它太容易了。Haskell不允许您放弃函数式编程,因此您必须将其总结为合乎逻辑的结论,这是一个易于推理的最终程序,可以避免一整类最棘手的错误。

在编写供现实世界使用的程序时,您可能会发现Haskell缺乏实用性,但最终的解决方案对于了解Haskell会更好。我绝对还没有,但是到目前为止,学习Haskell比说Lisp上大学要有意义得多。


1
好吧,总有可能永远只使用ST monad和/或unsafePerformIO只想观看世界燃烧的人;)
sara

22

大惊小怪的一部分是,纯净和静态类型化使并行性与积极的优化相结合。并行语言现在很热门,多核有点破坏性。

与几乎所有通用语言相比,Haskell为您提供了更多的并行性选项,并提供了快速的本机代码编译器。对并行样式的这种支持确实没有竞争:

因此,如果您关心使多核工作正常,那么Haskell有话要说。Simon Peyton Jones的Haskell并行和并发编程教程是一个很好的起点。


“以及快速的本机代码编译器”?
JD 2009年

我相信Dons是指GHCI。
格雷戈里·希格利2009年

3
@Jon:shootout.alioth.debian.org/u32/…例如, Haskell在枪战中表现出色。
Peaker

4
@Jon:枪战代码很老,而且源远流长,而GHC并不是最优化的编译器。尽管如此,它证明Haskell代码可以根据需要降低性能。枪战中的较新解决方案更加惯用且仍然快速。
Peaker

1
@GregoryHigley GHCI和GHC之间有区别。
杰里米·

18

软件事务性内存是处理并发的一种很酷的方法。它比消息传递更加灵活,并且不会像互斥锁那样容易发生死锁。 GHC实施STM被认为是最好的方法之一。


18

去年我花了很多时间学习Haskell,并在其中编写了一个相当大而复杂的项目。(该项目是一个自动的期权交易系统,从交易算法到解析和处理低水平,高速市场数据馈送的所有工作均在Haskell中完成。)它更加简洁明了(对于那些拥有适当的背景)和Java版本一样强大。

对我而言,最大的赢家可能是能够将诸如monoid,monad等之类的控制流模块化。一个非常简单的示例是Ordering monoid;在诸如

c1 `mappend` c2 `mappend` c3

其中c1等返回LTEQ或者GTc1返回EQ原因表达继续,评估c2; if c2返回LTGT那是整体的价值,并且c3不被评估。这种事情在诸如monadic消息生成器和解析器之类的事情上变得更加复杂和复杂,在这些情况下,我可能会携带不同类型的状态,具有变化的中止条件,或者可能希望能够为任何特定的调用确定中止是否真正意味着“没有进一步的处理”或“最终返回错误,但继续进行处理以收集更多的错误消息”。

这些都是需要花费一些时间和精力的东西,因此对于那些尚不了解这些技术的人来说,很难为其说服力。我认为《关于Monads全部》教程很好地展示了这一方面,但是我不希望任何不熟悉该材料的人都会在仔细阅读第一甚至第三本书时就“理解”它。

无论如何,Haskell中还有很多其他好东西,但这是我经常看到的一个重要的东西,可能是因为它很复杂。


2
很有意思!您的自动交易系统总共输入了多少行Haskell代码?您如何处理容错能力以及获得了哪些性能结果?我最近一直在思考Haskell有潜力用于低延迟编程...
JD 2010年

12

对于一个有趣的示例,您可以查看:http : //en.literateprograms.org/Quicksort_(Haskell)

有趣的是看各种语言的实现。

与其他功能语言一样,Haskell如此有趣的原因是您必须对编程方式进行不同的思考。例如,您通常不使用for或while循环,而是使用递归。

如上所述,Haskell和其他功能语言在并行处理和编写可在多核上工作的应用程序方面表现出色。


2
递归就是炸弹。和模式匹配。
Ellery Newcomer 2009年

1
用功能语言编写时,摆脱for和while循环对我来说是最难的部分。:)
詹姆斯·布莱克

4
学习递归而不是循环思考也是我最难的部分。当它最终沉入水中时,它是我所经历过的最伟大的编程顿悟之一。
克里斯·康尼特

8
工作的Haskell程序员很少使用原始递归;大多数情况下,您使用诸如map和foldr之类的库函数。
Paul Johnson

18
我发现更有趣的是,Hoare的原始quicksort算法显然被混编成这种基于位置列表的形式,以便可以在Haskell中“优雅地”编写无用的低效率实现。如果您尝试在Haskell中编写一个真实的(就地)快速排序,您会发现它很丑陋。如果您尝试在Haskell中编写具有竞争力的通用quicksort,则会发现由于GHC垃圾收集器中的长期错误,实际上是不可能的。Hailing quicksort是Haskell乞belief信仰IMHO的一个很好的例子。
JD

8

我不能给你一个例子,我是OCaml的家伙,但是当我遇到像你这样的情况时,好奇心就占据了上风,我必须下载编译器/解释器,然后尝试一下。您可能会以更多的方式了解给定功能语言的优缺点。


1
不要忘记阅读编译器的源代码。这也将为您提供很多有价值的信息。
JD 2010年

7

在处理算法或数学问题时,我发现很酷的一件事是Haskell固有的对计算的惰性评估,这仅由于其严格的功能性质才有可能。

例如,如果要计算所有素数,则可以使用

primes = sieve [2..]
    where sieve (p:xs) = p : sieve [x | x<-xs, x `mod` p /= 0]

结果实际上是一个无限的列表。但是Haskell将从左到右对其进行评估,因此,只要您不尝试执行需要整个列表的操作,您仍然可以使用它,而不会使程序陷入无限,例如:

foo = sum $ takeWhile (<100) primes

总和小于100的素数。这有几个原因。首先,我只需要编写一个生成所有素数的素数函数,然后就可以使用素数了。在面向对象的编程语言中,我将需要某种方法来告诉函数在返回之前应计算多少个素数,或使用对象模拟无限列表的行为。另一件事是,总的来说,您最终编写的代码表示要计算的内容,而不是要按什么顺序评估事物-而是由编译器为您完成。

这不仅对无限列表很有用,实际上,在不需要进行不必要的评估时,它总是在您不知不觉中被使用。


2
这不是完全正确的。利用C#(一种面向对象的语言)的收益返回行为,您还可以声明按需评估的无限列表。
杰夫·耶茨(

2
好点子。您是正确的,我应该避免一味地用其他语言说明可以做什么和不能做什么。我认为我的示例是有缺陷的,但是我仍然认为您可以从Haskell的惰性评估方法中获益:默认情况下,它确实存在,并且无需程序员的任何努力。我相信,这是由于其功能性质和没有副作用。
蜡翼

8
您可能有兴趣阅读为什么“筛子” 不是 Eratosthenes筛子的原因:lambda-the-ultimate.org/node/3127
克里斯·康威2009年

@Chris:谢谢,这实际上是一篇非常有趣的文章!上面的素数函数不是我一直在自己的计算中使用的函数,因为它很慢。不过,本文提出了一个很好的观点,即检查所有数字是否为mod确实是一种不同的算法。
蜡翼

6

我同意其他人的观点,即看到一些小例子并不是炫耀Haskell的最佳方法。但是我还是会给一些。这是对Euler Project问题18和67的快速解决方案,要求您找到从底边到三角形顶点的最大和路径:

bottomUp :: (Ord a, Num a) => [[a]] -> a
bottomUp = head . bu
  where bu [bottom]     = bottom
        bu (row : base) = merge row $ bu base
        merge [] [_] = []
        merge (x:xs) (y1:y2:ys) = x + max y1 y2 : merge xs (y2:ys)

这是Lesh和Mitzenmacher编写的BubbleSearch算法的完整,可重用的实现。我用它来打包大型媒体文件以将其归档存储在DVD上而没有浪费:

data BubbleResult i o = BubbleResult { bestResult :: o
                                     , result :: o
                                     , leftoverRandoms :: [Double]
                                     }
bubbleSearch :: (Ord result) =>
                ([a] -> result) ->       -- greedy search algorithm
                Double ->                -- probability
                [a] ->                   -- list of items to be searched
                [Double] ->              -- list of random numbers
                [BubbleResult a result]  -- monotone list of results
bubbleSearch search p startOrder rs = bubble startOrder rs
    where bubble order rs = BubbleResult answer answer rs : walk tries
            where answer = search order
                  tries  = perturbations p order rs
                  walk ((order, rs) : rest) =
                      if result > answer then bubble order rs
                      else BubbleResult answer result rs : walk rest
                    where result = search order

perturbations :: Double -> [a] -> [Double] -> [([a], [Double])]
perturbations p xs rs = xr' : perturbations p xs (snd xr')
    where xr' = perturb xs rs
          perturb :: [a] -> [Double] -> ([a], [Double])
          perturb xs rs = shift_all p [] xs rs

shift_all p new' [] rs = (reverse new', rs)
shift_all p new' old rs = shift_one new' old rs (shift_all p)
  where shift_one :: [a] -> [a] -> [Double] -> ([a]->[a]->[Double]->b) -> b
        shift_one new' xs rs k = shift new' [] xs rs
          where shift new' prev' [x] rs = k (x:new') (reverse prev') rs
                shift new' prev' (x:xs) (r:rs) 
                    | r <= p    = k (x:new') (prev' `revApp` xs) rs
                    | otherwise = shift new' (x:prev') xs rs
                revApp xs ys = foldl (flip (:)) ys xs

我确定这段代码看起来像乱码。但是,如果您阅读了Mitzenmacher的博客条目并理解了该算法,您会惊奇地发现,可以将算法打包到代码中而无需说任何您要搜索的内容。

在给您一些示例后,我将说,开始欣赏Haskell最佳方法是阅读这篇给了我写DVD包装机所需想法的论文:约翰休斯(John Hughes)为什么要进行功能编程。该论文实际上早于Haskell,但它很好地解释了一些使Haskell成为人们的想法。


5

对我来说,哈斯克尔的吸引力是编译器的承诺保证正确性。即使是纯粹的代码部分。

我已经写了很多的科学模拟代码,并想知道这么多次,如果有在我以前的代码中的错误,这可能无效了很多当前的工作。


6
如何保证正确性?
乔纳森·费斯霍夫

纯粹的代码部分比不纯的部分要安全得多。信任/努力的程度更高。
角色扮演游戏

1
是什么给您的印象?
JD 2010年

5

我发现对于某些任务,Haskell的工作效率非常高。

原因是因为语法简洁且易于测试。

这就是函数声明语法:

foo a = a + 5

这是定义函数的最简单方法。

如果我写反

inverseFoo a = a-5

我可以通过写来检查它是否是任何随机输入的逆

prop_IsInverse :: Double->布尔
prop_IsInverse a = a ==(inverseFoo $ foo a)

并从命令行调用

jonny @ ubuntu:runhaskell quickCheck +名称fooFileName.hs

通过随机测试输入的一百次,它将检查文件中的所有属性是否都保留。

我认为Haskell并不是适用于所有事物的完美语言,但是当涉及到编写小的函数和测试时,我没有发现更好的东西。如果您的程序具有数学成分,则这非常重要。


您正在解决什么问题,并且尝试过其他哪种语言?
JD

1
适用于移动设备和iPad的实时3d图形。
乔纳森·费斯霍夫

3

如果您可以全神贯注于Haskell的类型系统,那么我认为它本身就是一个成就。


1
那里有什么?如果必须,请考虑“数据” ==“类”和“类型类” =“接口” /“角色” /“特征”。这再简单不过了。(甚至没有“ null”
会把

8
jrockway有很多东西可以得到。当您和我发现它相对简单时,许多人-甚至许多开发人员-都很难理解某些抽象。我知道许多开发人员即使每天都使用指针和引用,但他们仍然不太掌握使用更多主流语言的指针和引用的概念。
格雷戈里·希格利2009年

2

它没有循环结构。没有多少种语言具有这种特征。


17
ghci>:m + Control.Monad ghci> forM_ [1..3]打印1 2 3
sastanin's

1

我同意那些说函数式编程使您的大脑从不同角度观看程序的观点。我只是将它用作业余爱好者,但我认为它从根本上改变了我处理问题的方式。我认为,如果不接触Haskell(并在Python中使用生成器和列表推导),我对LINQ的效果将几乎没有达到。


-1

提出相反的观点:Steve Yegge写道Hindely-Milner语言缺乏编写良好系统所需的灵活性

HM非常漂亮,完全没有形式化的数学意义。它很好地处理了一些计算结构;在Haskell,SML和OCaml中找到的模式匹配调度特别方便。毫不奇怪,它充其量只能处理笨拙的其他一些常见且非常可取的构造,但是它们通过说错了,实际上并不需要它们来解释这些情况。您知道,设置变量等。

Haskell值得学习,但是有其自身的弱点。


5
虽然强类型系统通常确实需要您遵守它们(这是使它们的强度有用的原因),但实际上,许多(大多数?)基于HM的类型系统确实具有某种“链接中所述的“逃生舱口”(以O'Caml中的Obj.magic为例,尽管我从来没有用过它,除非作为hack)。但是实际上,对于许多程序来说,人们从不需要这样的设备。
Zach Snow

3
设置变量是否“合乎需要”的问题取决于使用替代构造会带来多少痛苦,以及使用变量会带来多少痛苦。这并不是要消除整个论点,而是要指出,以“变量是一个非常可取的构造”为公理并不是强硬论点的基础。这恰好是大多数人学习编程的方式。
gtd

5
-1:史蒂夫的说法有些过时,但实际上基本上完全是错误的。OCaml的宽松值限制和.NET的类型系统是他的陈述中明显的反例。
JD

4
史蒂夫·耶格(Steve Yegge)的帽子上关于静态类型的问题是不合理的,不仅他所说的大多数错误,而且他还在每一个可用的机会(甚至是一些不可用的机会)上不断提出建议。您最好只相信自己在这方面的经验。
ShreevatsaR

3
尽管我不同意Yegge的静态类型和动态类型,但Haskell确实具有Data.Dynamic类型。如果要动态输入,可以使用它!
jrockway
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.