【问题标题】:What's the fuss about Haskell? [closed]Haskell 有什么大惊小怪的? [关闭]
【发布时间】:2023-03-14 16:40:01
【问题描述】:

我认识一些程序员,他们在彼此之间不断谈论 Haskell,而且似乎每个人都喜欢这种语言。擅长 Haskell 似乎有点像天才程序员的标志。

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

【问题讨论】:

    标签: haskell functional-programming


    【解决方案1】:

    这是说服我学习 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 版本慢,尤其是没有预取......
    • @Mehrdad 您可以使用hSetBuffering handle (BlockBuffering (Just bufferSize)).更改缓冲区大小
    • 令人惊讶的是,这个答案有 116 个赞成票,但其中的内容是错误的。这个程序读取整个文件,除非你使用惰性字节串(你可以使用Data.Bytestring.Lazy.readFile),这与 Haskell 是一种惰性(非严格)语言无关。 Monad 是 sequencing —— 这大致意味着“当你取出结果时,所有的副作用都完成了”。至于“惰性字节串”魔法:这很危险,您可以使用大多数其他语言中类似或更简单的语法来做到这一点。
    • 无聊的旧标准readFile 也像Data.ByteString.Lazy.readFile 一样做惰性IO。所以答案没有错,而且不仅仅是编译器优化。事实上,这是the spec for Haskell 的一部分:“readFile 函数读取文件并将文件内容作为字符串返回。文件是按需延迟读取的,就像getContents 一样。”
    • 我认为其他答案指向 Haskell 更特别的地方。许多语言/环境都有流,你可以在 Node:const fs = require('fs'); const [file1, file2] = process.argv.slice(2); fs.createReadStream(file1).pipe(fs.createWriteStream(file2)) 中做类似的事情。 Bash 也有类似的东西:cat $1 &gt; $2
    【解决方案2】:

    向我介绍它的方式,以及在学习 Haskell 一个月后我认为是正确的事实是,函数式编程以有趣的方式扭曲了你的大脑:它迫使你思考熟悉的问题以不同的方式:而不是循环,考虑地图、折叠和过滤器等。一般来说,如果你对一个问题有多个观点,它会让你更好地推理这个问题,并在必要时切换观点。

    关于 Haskell 的另一个非常巧妙的地方是它的类型系统。它是严格类型化的,但是类型推断引擎让它感觉就像一个 Python 程序,当你犯了一个愚蠢的类型相关错误时,它会神奇地告诉你。 Haskell 在这方面的错误信息有些欠缺,但是当你对这种语言更加熟悉时,你会对自己说:这就是打字应该是的!

    【讨论】:

    • 需要注意的是,Haskell 的错误信息不缺,ghc 的缺。 Haskell 标准没有指定错误消息的处理方式。
    • 对于像我这样的普通人来说,GHC 代表 Glasgow Haskell Compiler。 en.wikipedia.org/wiki/Glasgow_Haskell_Compiler
    【解决方案3】:

    你问错问题了。

    Haskell 不是一种语言,你可以通过查看一些很酷的示例然后说“啊哈,我现在明白了,这就是它的优点!”

    这更像是,我们拥有所有这些其他编程语言,它们或多或少都相似,然后是 Haskell,它完全不同且古怪,一旦你习惯了古怪,就会非常棒。但问题是,要适应这种古怪需要相当长的时间。使 Haskell 与几乎任何其他甚至是半主流语言不同的地方:

    • 懒惰评估
    • 没有副作用(一切都是纯粹的,IO/etc 通过 monad 发生)
    • 极具表现力的静态类型系统

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

    • 功能性
    • 重要的空格
    • 类型推断

    正如其他一些发帖人所回答的那样,所有这些功能的组合意味着您以完全不同的方式思考编程。所以很难想出一个例子(或一组例子)来充分地将这一点传达给 Joe-mainstream-programmer。这是一个经验的事情。 (打个比方,我可以给你看我1970年去中国旅行的照片,但看了照片你还是不知道那段时间在那里生活是什么感觉。同样,我可以给你看一个Haskell '快速排序',但你仍然不会知道成为 Haskeller 意味着什么。)

    【讨论】:

    • 我不同意你的第一句话。最初,我对一些 Haskell 代码示例印象深刻,真正让我相信值得学习的是这篇文章:cs.dartmouth.edu/~doug/powser.html 当然,这对数学家/物理学家来说很有趣。研究现实世界的程序员会觉得这个例子很荒谬。
    • @Rafael:这就引出了一个问题“程序员在研究现实世界的东西时会被什么打动”?
    • 好问题!我不是“现实世界”的程序员,所以我不知道他们喜欢什么。哈哈哈……我知道物理学家和数学家喜欢什么。 :P
    【解决方案4】:

    真正让 Haskell 与众不同的是它在设计中努力执行函数式编程。您可以使用几乎任何语言以函数式风格进行编程,但在第一次方便时就很容易放弃。 Haskell 不允许您放弃函数式编程,因此您必须将其得出合乎逻辑的结论,这是一个更容易推理的最终程序,并且回避了一整类最棘手的错误类型。

    在编写用于现实世界的程序时,您可能会发现 Haskell 缺乏一些实用的方式,但如果您从一开始就了解 Haskell,您的最终解决方案会更好。我肯定还没有,但到目前为止,学习 Haskell 比 Lisp 上大学时更有启发性。

    【讨论】:

    • 好吧,对于那些只想看着世界燃烧的人来说,总是有可能只使用 ST monad 和/或 unsafePerformIO ;)
    【解决方案5】:

    大惊小怪的部分原因在于纯度和静态类型可以实现并行性和积极的优化。并行语言现在很热门,多核有点颠覆性。

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

    因此,如果您关心让您的多核工作,Haskell 有话要说。 一个很好的起点是 Simon Peyton Jones 的tutorial on parallel and concurrent programming in Haskell

    【讨论】:

    • “以及快速的本地代码编译器”?
    • 我相信 dons 指的是 GHCI。
    • @Jon: shootout.alioth.debian.org/u32/… 例如,Haskell 在枪战中的表现非常出色。
    • @Jon:枪战代码非常古老,而且在遥远的过去,GHC 还不是一个优化编译器。尽管如此,它仍然证明了 Haskell 代码可以在需要时进入低级以产生性能。枪战中较新的解决方案更惯用,而且速度仍然很快。
    • @GregoryHigley GHCI 和 GHC 是有区别的。
    【解决方案6】:

    Software Transactional Memory 是一种非常酷的并发处理方式。它比消息传递灵活得多,而且不像互斥锁那样容易出现死锁。 GHC's STM 的实现被认为是最好的实现之一。

    【讨论】:

      【解决方案7】:

      去年我一直在学习 Haskell,并用它编写了一个相当大且复杂的项目。 (该项目是一个自动期权交易系统,从交易算法到解析和处理低级、高速市场数据馈送的一切都在 Haskell 中完成。)它更加简洁和易于理解(对于那些有适当的背景),而不是 Java 版本,并且非常健壮。

      可能对我来说最大的胜利是能够通过诸如 monoids、monads 之类的东西来模块化控制流。一个非常简单的例子是 Ordering monoid;在诸如

      之类的表达式中
      c1 `mappend` c2 `mappend` c3
      

      其中c1 等返回LTEQGTc1 返回EQ 导致表达式继续,计算c2;如果c2 返回LTGT,那就是整体的值,而c3 不会被评估。这种事情在诸如单子消息生成器和解析器之类的东西中变得相当复杂和复杂,我可能携带不同类型的状态,具有不同的中止条件,或者可能希望能够为任何特定调用决定中止是否真的意味着“没有进一步处理”或意思是“最后返回错误,但继续处理以收集更多错误消息。”

      所有这些东西都需要一些时间并且可能需要相当多的努力才能学习,因此对于那些还不了解这些技术的人来说,很难为它做出令人信服的论据。我认为All About Monads 教程在其中一个方面给出了令人印象深刻的演示,但我不希望任何不熟悉该材料的人在第一次甚至第三次仔细阅读时都会“理解”。

      无论如何,Haskell 中还有很多其他好东西,但这是我不经常提到的主要内容,可能是因为它相当复杂。

      【讨论】:

      • 非常有趣!总共有多少行 Haskell 代码进入您的自动交易系统?你是如何处理容错的,你得到了什么样的性能结果?我最近一直在想,Haskell 有潜力成为低延迟编程的好帮手……
      【解决方案8】:

      你可以看一个有趣的例子: http://en.literateprograms.org/Quicksort_(Haskell)

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

      使 Haskell 以及其他函数式语言如此有趣的原因在于,您必须以不同的方式思考如何编程。例如,您通常不会使用 for 或 while 循环,但会使用递归。

      如上所述,Haskell 和其他函数式语言在并行处理和编写多核应用程序方面表现出色。

      【讨论】:

      • 递归是炸弹。那和模式匹配。
      • 摆脱 for 和 while 循环是我用函数式语言编写时最困难的部分。 :)
      • 学习用递归而不是循环来思考对我来说也是最难的部分。当它最终沉入其中时,这是我经历过的最伟大的编程顿悟之一。
      • 除了工作中的 Haskell 程序员很少使用原始递归;大多数情况下,您使用诸如 map 和 foldr 之类的库函数。
      • 我发现更有趣的是,Hoare 的原始快速排序算法显然被混入了这种不合时宜的基于列表的形式,以便可以在 Haskell 中“优雅地”编写无用的低效实现。如果您尝试在 Haskell 中编写一个真正的(就地)快速排序,您会发现它非常丑陋。如果您尝试在 Haskell 中编写具有竞争力的通用快速排序,您会发现由于 GHC 垃圾收集器中长期存在的错误,这实际上是不可能的。恕我直言,将快速排序作为 Haskell 乞丐信仰的一个很好的例子。
      【解决方案9】:

      我不能给你一个例子,我是一个 OCaml 人,但是当我处于像你这样的情况时,好奇心就会占据上风,我必须下载一个编译器/解释器并试一试。通过这种方式,您可能会更多地了解给定函数式语言的优缺点。

      【讨论】:

      • 别忘了阅读编译器的源代码。这也会给你很多有价值的信息。
      【解决方案10】:

      在处理算法或数学问题时,我觉得很酷的一件事是 Haskell 对计算的固有惰性求值,这只能归功于其严格的函数性质。

      例如,如果你想计算所有素数,你可以使用

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

      结果实际上是一个无限列表。但是 Haskell 会从左到右求值,所以只要你不尝试做需要整个列表的事情,你仍然可以使用它而不会让程序陷入无穷大,例如:

      foo = sum $ takeWhile (<100) primes
      

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

      这不仅对无限列表有用,事实上,当您不需要评估超过必要的值时,它会一直在您不知道的情况下使用。

      【讨论】:

      • 这并不完全正确;借助 C#(一种面向对象的语言)的 yield return 行为,您还可以声明按需评估的无限列表。
      • 好点。你是对的,我应该避免如此断然地说明用其他语言可以做什么和不能做什么。我认为我的示例有缺陷,但我仍然认为您从 Haskell 的惰性求值方式中获得了一些东西:默认情况下它确实存在,无需程序员的任何努力。我相信,这是由于它的功能性和没有副作用。
      • 您可能有兴趣了解为什么“筛子”不是埃拉托色尼的筛子:lambda-the-ultimate.org/node/3127
      • @Chris:谢谢,这实际上是一篇非常有趣的文章!上面的 primes 函数不是我用于自己计算的函数,因为它非常慢。尽管如此,这篇文章还是提出了一个很好的观点,即检查所有数字的 mod 确实是一种不同的算法。
      【解决方案11】:

      我同意其他人的观点,即看几个小例子并不是展示 Haskell 的最佳方式。但无论如何我都会给一些。这是Euler Project problems 18 and 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's blog entry 并理解了该算法,您会惊奇地发现可以将算法打包到代码中而无需说明您要搜索的内容。

      按照你的要求给了你一些例子,我会说开始欣赏 Haskell 的最佳方法是阅读这篇论文,它给了我编写 DVD 打包程序所需的想法: Why Functional Programming Matters 约翰·休斯。这篇论文实际上早于 Haskell,但它出色地解释了一些让人们喜欢 Haskell 的想法。

      【讨论】:

        【解决方案12】:

        对我来说,Haskell 的吸引力在于对编译器保证正确性的承诺。即使是纯代码部分。

        我已经编写了很多科学模拟代码,并且多次想这么在我之前的代码中是否存在错误,这可能会使当前的许多工作无效。

        【讨论】:

        • 如何保证正确性?
        • 纯代码部分比不纯代码部分安全得多。信任水平/投入的精力要高得多。
        • 是什么给了你这样的印象?
        【解决方案13】:

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

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

        函数声明语法是这样的:

        foo a = a + 5

        这是我能想到的定义函数的最简单方法。

        如果我反写

        inverseFoo a = a - 5

        我可以通过写来检查它是否与任何随机输入相反

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

        从命令行调用

        jonny@ubuntu: runhaskell quickCheck +names fooFileName.hs

        这将通过随机测试输入一百次来检查我文件中的所有属性是否被保存。

        我不认为 Haskell 是适合所有事物的完美语言,但是在编写小函数和测试方面,我没有见过更好的语言。如果您的程序包含数学部分,这一点非常重要。

        【讨论】:

        • 你解决了什么问题,你尝试过哪些其他语言?
        • 适用于移动设备和 iPad 的实时 3D 图形。
        【解决方案14】:

        如果您能深入了解 Haskell 中的类型系统,我认为这本身就是一项成就。

        【讨论】:

        • 有什么可以得到的?如果必须,请考虑“数据”==“类”和“类型类”=“接口”/“角色”/“特征”。再简单不过了。 (甚至没有“null”来搞砸你。Null 是一个你可以自己构建到你的类型中的概念。)
        • 有很多东西等着你,jrockway。虽然你和我都觉得它相对简单,但许多人——甚至许多开发人员——发现某些类型的抽象非常难以理解。我知道许多开发人员仍然不太了解更主流语言中的指针和引用的概念,即使他们每天都在使用它们。
        【解决方案15】:

        它没有循环结构。没有多少语言有这个特点。

        【讨论】:

        • ghci> :m + Control.Monad ghci> forM_ [1..3] print 1 2 3
        【解决方案16】:

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

        【讨论】:

          【解决方案17】:

          表达逆向观点:Steve Yegge 写道Hindely-Milner languages lack the flexibility required to write good systems

          H-M 很漂亮,完全 无用的形式数学意义。它 处理一些计算结构 很友好地;模式匹配 在 Haskell、SML 和 OCaml 特别好用。 不出所料,它处理其他一些 常见且非常理想的构造 充其量是尴尬,但他们解释说 说那些场景 你错了,你实际上并没有 要他们。你知道,像,哦, 设置变量。

          Haskell 值得学习,但它也有自己的弱点。

          【讨论】:

          • 虽然强类型系统通常需要你遵守它们当然是真的(这就是它们的优势有用的原因),但许多(大多数?)现有的基于 HM 的类型系统也是如此,实际上,如链接中所述,具有某种“逃生舱口”(以 O'Caml 中的 Obj.magic 为例,尽管我从未将其用作黑客工具);然而,在实践中,对于许多类型的程序来说,人们永远不需要这样的设备。
          • 设置变量是否“可取”的问题取决于使用替代结构的痛苦与使用变量造成的痛苦。这并不是要驳回整个论点,而是要指出,将“变量是一个非常理想的构造”作为公理并不是一个有说服力的论点的基础。这恰好是大多数人学习编程的方式。
          • -1:史蒂夫的陈述部分已经过时,但大多数情况下完全是错误的。 OCaml 宽松的值限制和 .NET 的类型系统是他陈述的一些明显反例。
          • Steve Yegge 对静态类型有一种不合理的看法,不仅他所说的大部分内容都是错误的,而且他还在每一个可用的机会(甚至是一些不可用的机会)中不断提出它)。在这方面你最好只相信你自己的经验。
          • 虽然我不同意 Yegge 关于静态和动态类型的观点,但 Haskell 确实有 Data.Dynamic 类型。如果你想要动态类型,你可以拥有它!
          猜你喜欢
          • 2011-09-02
          • 2016-05-16
          • 2011-12-10
          • 1970-01-01
          • 1970-01-01
          • 2014-11-04
          • 2018-08-08
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多