【发布时间】:2023-03-14 16:40:01
【问题描述】:
我认识一些程序员,他们在彼此之间不断谈论 Haskell,而且似乎每个人都喜欢这种语言。擅长 Haskell 似乎有点像天才程序员的标志。
有人可以举几个 Haskell 的例子来说明它为什么如此优雅/优越吗?
【问题讨论】:
标签: haskell functional-programming
我认识一些程序员,他们在彼此之间不断谈论 Haskell,而且似乎每个人都喜欢这种语言。擅长 Haskell 似乎有点像天才程序员的标志。
有人可以举几个 Haskell 的例子来说明它为什么如此优雅/优越吗?
【问题讨论】:
标签: haskell functional-programming
这是说服我学习 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 代码的紧凑性,至少有以下三个原因:
更好的程序设计。降低复杂性会减少逻辑错误。
压缩代码。存在错误的行数更少。
编译错误。很多错误只是不是有效的 Haskell。
Haskell 并不适合所有人。但每个人都应该尝试一下。
【讨论】:
hSetBuffering handle (BlockBuffering (Just bufferSize)).更改缓冲区大小
Data.Bytestring.Lazy.readFile),这与 Haskell 是一种惰性(非严格)语言无关。 Monad 是 sequencing —— 这大致意味着“当你取出结果时,所有的副作用都完成了”。至于“惰性字节串”魔法:这很危险,您可以使用大多数其他语言中类似或更简单的语法来做到这一点。
readFile 也像Data.ByteString.Lazy.readFile 一样做惰性IO。所以答案没有错,而且不仅仅是编译器优化。事实上,这是the spec for Haskell 的一部分:“readFile 函数读取文件并将文件内容作为字符串返回。文件是按需延迟读取的,就像getContents 一样。”
const fs = require('fs'); const [file1, file2] = process.argv.slice(2); fs.createReadStream(file1).pipe(fs.createWriteStream(file2)) 中做类似的事情。 Bash 也有类似的东西:cat $1 > $2
向我介绍它的方式,以及在学习 Haskell 一个月后我认为是正确的事实是,函数式编程以有趣的方式扭曲了你的大脑:它迫使你思考熟悉的问题以不同的方式:而不是循环,考虑地图、折叠和过滤器等。一般来说,如果你对一个问题有多个观点,它会让你更好地推理这个问题,并在必要时切换观点。
关于 Haskell 的另一个非常巧妙的地方是它的类型系统。它是严格类型化的,但是类型推断引擎让它感觉就像一个 Python 程序,当你犯了一个愚蠢的类型相关错误时,它会神奇地告诉你。 Haskell 在这方面的错误信息有些欠缺,但是当你对这种语言更加熟悉时,你会对自己说:这就是打字应该是的!
【讨论】:
你问错问题了。
Haskell 不是一种语言,你可以通过查看一些很酷的示例然后说“啊哈,我现在明白了,这就是它的优点!”
这更像是,我们拥有所有这些其他编程语言,它们或多或少都相似,然后是 Haskell,它完全不同且古怪,一旦你习惯了古怪,就会非常棒。但问题是,要适应这种古怪需要相当长的时间。使 Haskell 与几乎任何其他甚至是半主流语言不同的地方:
以及其他一些与许多主流语言不同的方面(但被一些人所共有):
正如其他一些发帖人所回答的那样,所有这些功能的组合意味着您以完全不同的方式思考编程。所以很难想出一个例子(或一组例子)来充分地将这一点传达给 Joe-mainstream-programmer。这是一个经验的事情。 (打个比方,我可以给你看我1970年去中国旅行的照片,但看了照片你还是不知道那段时间在那里生活是什么感觉。同样,我可以给你看一个Haskell '快速排序',但你仍然不会知道成为 Haskeller 意味着什么。)
【讨论】:
真正让 Haskell 与众不同的是它在设计中努力执行函数式编程。您可以使用几乎任何语言以函数式风格进行编程,但在第一次方便时就很容易放弃。 Haskell 不允许您放弃函数式编程,因此您必须将其得出合乎逻辑的结论,这是一个更容易推理的最终程序,并且回避了一整类最棘手的错误类型。
在编写用于现实世界的程序时,您可能会发现 Haskell 缺乏一些实用的方式,但如果您从一开始就了解 Haskell,您的最终解决方案会更好。我肯定还没有,但到目前为止,学习 Haskell 比 Lisp 上大学时更有启发性。
【讨论】:
unsafePerformIO ;)
大惊小怪的部分原因在于纯度和静态类型可以实现并行性和积极的优化。并行语言现在很热门,多核有点颠覆性。
Haskell 为您提供了比几乎任何通用语言更多的并行选项,以及快速的本机代码编译器。这种对并行样式的支持确实没有竞争对手:
因此,如果您关心让您的多核工作,Haskell 有话要说。 一个很好的起点是 Simon Peyton Jones 的tutorial on parallel and concurrent programming in Haskell。
【讨论】:
Software Transactional Memory 是一种非常酷的并发处理方式。它比消息传递灵活得多,而且不像互斥锁那样容易出现死锁。 GHC's STM 的实现被认为是最好的实现之一。
【讨论】:
去年我一直在学习 Haskell,并用它编写了一个相当大且复杂的项目。 (该项目是一个自动期权交易系统,从交易算法到解析和处理低级、高速市场数据馈送的一切都在 Haskell 中完成。)它更加简洁和易于理解(对于那些有适当的背景),而不是 Java 版本,并且非常健壮。
可能对我来说最大的胜利是能够通过诸如 monoids、monads 之类的东西来模块化控制流。一个非常简单的例子是 Ordering monoid;在诸如
之类的表达式中c1 `mappend` c2 `mappend` c3
其中c1 等返回LT、EQ 或GT、c1 返回EQ 导致表达式继续,计算c2;如果c2 返回LT 或GT,那就是整体的值,而c3 不会被评估。这种事情在诸如单子消息生成器和解析器之类的东西中变得相当复杂和复杂,我可能携带不同类型的状态,具有不同的中止条件,或者可能希望能够为任何特定调用决定中止是否真的意味着“没有进一步处理”或意思是“最后返回错误,但继续处理以收集更多错误消息。”
所有这些东西都需要一些时间并且可能需要相当多的努力才能学习,因此对于那些还不了解这些技术的人来说,很难为它做出令人信服的论据。我认为All About Monads 教程在其中一个方面给出了令人印象深刻的演示,但我不希望任何不熟悉该材料的人在第一次甚至第三次仔细阅读时都会“理解”。
无论如何,Haskell 中还有很多其他好东西,但这是我不经常提到的主要内容,可能是因为它相当复杂。
【讨论】:
你可以看一个有趣的例子: http://en.literateprograms.org/Quicksort_(Haskell)
有趣的是查看各种语言的实现。
使 Haskell 以及其他函数式语言如此有趣的原因在于,您必须以不同的方式思考如何编程。例如,您通常不会使用 for 或 while 循环,但会使用递归。
如上所述,Haskell 和其他函数式语言在并行处理和编写多核应用程序方面表现出色。
【讨论】:
我不能给你一个例子,我是一个 OCaml 人,但是当我处于像你这样的情况时,好奇心就会占据上风,我必须下载一个编译器/解释器并试一试。通过这种方式,您可能会更多地了解给定函数式语言的优缺点。
【讨论】:
在处理算法或数学问题时,我觉得很酷的一件事是 Haskell 对计算的固有惰性求值,这只能归功于其严格的函数性质。
例如,如果你想计算所有素数,你可以使用
primes = sieve [2..]
where sieve (p:xs) = p : sieve [x | x<-xs, x `mod` p /= 0]
结果实际上是一个无限列表。但是 Haskell 会从左到右求值,所以只要你不尝试做需要整个列表的事情,你仍然可以使用它而不会让程序陷入无穷大,例如:
foo = sum $ takeWhile (<100) primes
它对所有小于 100 的素数求和。这很好有几个原因。首先,我只需要编写一个生成所有素数的素数函数,然后我就可以使用素数了。在面向对象的编程语言中,我需要一些方法来告诉函数在返回之前它应该计算多少个素数,或者用一个对象模拟无限列表行为。另一件事是,一般来说,您最终编写的代码表达了您想要计算的内容,而不是按照什么顺序来评估事物 - 而是编译器为您完成。
这不仅对无限列表有用,事实上,当您不需要评估超过必要的值时,它会一直在您不知道的情况下使用。
【讨论】:
我同意其他人的观点,即看几个小例子并不是展示 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 的想法。
【讨论】:
对我来说,Haskell 的吸引力在于对编译器保证正确性的承诺。即使是纯代码部分。
我已经编写了很多科学模拟代码,并且多次想这么在我之前的代码中是否存在错误,这可能会使当前的许多工作无效。
【讨论】:
我发现对于某些任务,我使用 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 是适合所有事物的完美语言,但是在编写小函数和测试方面,我没有见过更好的语言。如果您的程序包含数学部分,这一点非常重要。
【讨论】:
如果您能深入了解 Haskell 中的类型系统,我认为这本身就是一项成就。
【讨论】:
它没有循环结构。没有多少语言有这个特点。
【讨论】:
我同意那些说函数式编程会扭曲你的大脑以从不同角度看待编程的观点。我只是将它用作业余爱好者,但我认为它从根本上改变了我处理问题的方式。如果没有接触过 Haskell(以及在 Python 中使用生成器和列表推导式),我认为我使用 LINQ 几乎不会如此有效。
【讨论】:
表达逆向观点:Steve Yegge 写道Hindely-Milner languages lack the flexibility required to write good systems:
H-M 很漂亮,完全 无用的形式数学意义。它 处理一些计算结构 很友好地;模式匹配 在 Haskell、SML 和 OCaml 特别好用。 不出所料,它处理其他一些 常见且非常理想的构造 充其量是尴尬,但他们解释说 说那些场景 你错了,你实际上并没有 要他们。你知道,像,哦, 设置变量。
Haskell 值得学习,但它也有自己的弱点。
【讨论】: