三个月的行程(从哈斯克勒的角度看)

2020-11-04 11:30:27

这个夏天我一直在Pusher实习,写了很多围棋。哈斯克尔的背景有点改变,所以我决定在最后写下我的想法。

没什么可去的,它是一种相当小的语言。在六月之前我一行也没有写过,现在我已经写了大约三万篇了。入门并变得富有成效是非常容易的。

另一方面,Haskell以难学而臭名昭著(咳嗽单曲教程咳嗽)。人们经常发现很难从计算纯数学表达式到编写实际的程序。我在围棋中没有经历过这样的脱节。

Pusher之前曾试图在我参与的项目中使用Haskell,但最终由于垃圾收集暂停导致的不可预测的延迟而不得不放弃。GHC的垃圾收集器设计用于吞吐量,而不是延迟。它是分代复制收集器,这意味着暂停时间与堆中的实时数据量成正比。更糟糕的是,这也是在阻止世界。

Go的垃圾收集器是一个带有非常短的停止-世界暂停的并发标记-清扫,而且它似乎一直在变得更好。对于垃圾收集语言来说,这绝对是件好事。我们确实有一些延迟无法接受的问题,但我们能够全部解决。哈斯克尔项目就没有这么好的运气了。

尽管您喜欢gofmt,但它使得围绕代码样式的争论几乎是不可能的。只需在保存时运行它,您的代码将始终保持一致的格式。

我确实觉得有点奇怪,gofmt已经被完全接受,而Python的重要空格(存在的原因完全相同:强制执行可读代码)在编程社区中更具争议性。

我不是代码生成的狂热爱好者(我是作为代码生成工具的作者说这番话的)。我认为它可以做好事,但它也可能掩盖实际发生的事情。在每次关于Go泛型的讨论中,都会有人说您可以通过代码生成来添加泛型:这是真的,但代价是引入了额外的非标准语法。

我怀疑代码生成的强大文化在很大程度上是因为它让您可以绕过语言的缺陷。

严格的计算通常比懒惰的计算更有利于性能(块会导致分配,因此您在赌节省的计算会抵消内存成本),但它确实会降低可组合性。有几次我去拆分一个函数,结果才意识到这样做需要在内存中分配一个以前不需要的数据结构。

我可以相信编译器会为我内联程序,从而优化掉额外的分配,但是在懒惰的语言中根本就没有这个问题。

如果你了解我本人,我特别评论这件事可能有点奇怪。通常情况下,我完全支持拥有一个很小的、编写得非常好的stdlib的语言,以及通过库提供的所有其他东西。我选择Go Here有点是因为标准库似乎得到了很多赞誉,但我不为所动。

它有一部分是好的,有很多是平庸的,而有些则是彻头彻尾的糟糕(比如Go/ast包文档)。似乎Go在WebDev中有很多用处,所以也许stdlib的那些部分(我根本没有碰过它)一直都很好。

我也同意Tikhon Jelvis的Quora回答,你觉得Golang很丑吗?所以一旦你读完这一节,就来看看这一点。

在Go中,您可以通过URL导入包。如果URL指向(比方说)GitHub,那么Go Get将下载主站点的负责人并使用该站点。除非您的库的每个版本都有单独的URL,否则无法指定版本。

围棋有非常强的向后兼容文化,我认为这在很大程度上是因为这一点。即使您的库的API中有一个缺陷,您也不能实际修复它,因为这将打破您所有的反向依赖,除非它们确实提供或固定到特定的提交。

来自哈斯克尔世界,那里的态度更多的是对正确的态度,而不是兼容性,这可能是对我最大的文化冲击。在Haskell中破坏了向后兼容性,用户只是更新他们的代码,因为他们知道库作者这样做是有原因的。在围棋中,这根本不会发生。

哈斯克尔的一句俗语是“让非法国家变得不可代表”,这很棒。如果您以前从未遇到过它,那么它意味着选择您的类型,这样非法的值就是静态错误。想要避免空值吗?使用选项类型。想要确保列表至少有一个元素吗?使用非空列表类型。使用正确的枚举,而不仅仅是整数。等,等等。

在围棋中你不能这样做,类型系统不够强大。因此,Haskell中的许多事情(或可能是)编译时错误都是Go中的运行时错误,这只是更糟糕的错误。

想要编写一个静态保证每个元素都是同一类型的树吗?那么,享受实现“uinttree”、“inttree”、“string tree”等等的乐趣吧。您不能只实现泛型树。

但是Go确实有针对内置类型的泛型。数组、通道、贴图和切片都有泛型类型参数。因此,Go开发人员似乎想要泛型,但他们不想费心正确地实现它,因此它仍然是编译器中一些东西的特例。

在GO中处理可能失败的函数的方法是有多个返回值:实际结果和错误。如果误差为零,则实际结果是合理的;否则,实际结果是没有意义的。

这意味着您可以忘记检查错误并使用假结果,而且因为没有编译器警告(另一个wtf),所以在运行时失败之前,您将对此一无所知。

仅仅通过查看函数的类型,就可以知道它不能执行任何副作用,这是非常好的。围棋的类型系统不能做到这一点。

Haskell因为糟糕的工具而受到很多批评,但我认为在某些情况下这是遥不可及的。

Godoc按类型对绑定进行分组,然后按字母顺序排序。代码不是这样写的,代码是用彼此接近的相关函数编写的。源顺序几乎总是比godoc对事物进行排序的方式要好。

之前类似的提议都被拒绝了,理由是从这里到降价或更糟的情况很糟糕。

我认为这一评论特别令人沮丧。因为开发人员不喜欢Markdown(以及类似的语言),他们甚至拒绝向godoc添加最基本的格式。

Go有一个基于快照的内存分析器。您可以在某个时间点拍摄快照,并查看哪些函数和类型占用了堆空间。然而,没有这样的事情。

不仅可以看到快照,还可以看到事情随着时间的推移发生了怎样的变化,这对于发现内存泄漏非常有用。如果您所拥有的只是一个快照,那么您真正能说的就是“嗯,分配的Foo的数量看起来有点高,对吗?”通过图表,您可以说“分配的foo数量正在增加,而不应该是这样。”

ThreadScope是一个分析并发Haskell程序性能的工具。它显示了哪些Haskell线程在哪些操作系统线程上运行,垃圾收集何时发生,以及一系列其他信息。

如果事情比预期的慢,那就太好了:您可以确切地看到事情是如何执行的。Go目前还没有类似的功能,尽管在英国GolangUK的Dave Cheney‘s Seven Ways to Profile Go Applications演讲接近尾声时,他确实拿出了一些看起来很像ThreadScope的东西(遗憾的是,在我写这篇文章的时候,还没有上传一段视频,我可以看到)。

GO通过使用“零值”避免了未初始化内存的问题。如果您声明了一个int类型的变量,但是没有给它赋值,那么它的值就是0。很简单。

什么是类型的合理默认值?嗯,这取决于你用它做什么!有时没有合理的默认值,不初始化值应该是错误的。你不能为你自己的类型定义一个零值,所以你有点卡住了。

零值在整个夏天造成了很多问题,因为一切看起来都很好,然后它突然崩溃了,因为零值对于它的使用环境来说是不明智的。也许是不相关的更改导致了事情的中断(就像结构获得了额外的字段)。

因为您必须检查错误值,所以如果您想要执行一系列可能出错的计算,其中一个计算的成功结果会输入到下一个计算中,则需要大量键入。在Haskell中,您只需使用任一Monad。

如果因为没有泛型而要对切片进行排序,则需要将切片包装在另一个类型中,并在该类型上实现三个方法。这是对一片uint进行排序的四行代码,对一片uint8进行排序的四行代码,对一段uint16进行排序的四行代码,依此类推。在Haskell中,您只需使用泛型排序。

我确实有一个非推手围棋项目,我计划继续发展,因为它是一个有趣的项目。我选择了Go,因为我最初的动机是最终将它与我在工作中所做的事情结合起来,但最终没有实现。

除此之外,我可能再也不会选择使用Go做任何事情了,除非我为此得到了报酬。GO与我的思考方式完全不同:当我处理编程问题时,我首先考虑的是有用的类型和抽象;我考虑的是静态强制行为;我不担心中间数据结构的成本,因为这个代价几乎从未完全支付过。