废了那么长时间,也该学拖延了两年的 Haskell 了。非常入门级的笔记。教材选择的是阅千人而惜知己的《Haskell 函数式编程λ门》,以及《Haskell 趣学指南》搭配着看,还有一些知乎上的文章用来参考。

《Haskell 函数式编程入门》这书需要勘误表,我觉得内容本身还是非常不错的。初学者的话可能看《Haskell 趣学指南》更平易近人一些。还有大名鼎鼎的《Real World Haskell》。

环境搭建

这里主要参考的是邵喵的《不动点高校迎新会》这篇文章。不过注意的是,构筑工具最好用 stack,而且国内已经有镜像了。如果遇到什么问题的话可以看这个讨论

就像文章里说的,Atom 搭配 ide-haskell 就很好,详细的配置可以参考《打造令人愉悦的 Haskell 开发环境》。

字体

还有一个小准备就是可以安装特殊的字体文件,Haskell 里面有很多特殊的符号,如果用了专门的字体看起来会很舒心,比如说 Hasklig 或者 be5 大大做的 Iosevka,以及配套的全 Unicode 等宽字体 Inziu

因为这些字体用到了 OpenType 的连笔字特性,Atom 里面默认是关闭的,所以要在 Atom 的样式表里面加入

atom-text-editor {
  text-rendering: optimizeLegibility;

  /* 仅当用 Iosevka / Inziu 时需要。 */
  -webkit-font-feature-settings: "XPTL";
}

foldrfoldl

看《趣学指南》的时候,把 foldrfoldl 说得有点简略了,说 foldr 是从列表的右边开始折叠的,foldl 是从左边。但是这就让人搞不懂为什么,对一个无穷列表 fold 的同时产生一个新的列表,foldl 会陷入无穷循环而 foldr 不会。

> take 10 $ foldr (\x z -> x+1:z) [] [1..]
[2,3,4,5,6,7,8,9,10,11]

> take 10 $ foldl (\z x -> x+1:z) [] [1..]
...

如果从列表的右边开始的话,对于 foldr 访问无穷列表最右端的元素是永远访问不到的,反而应该陷入无穷循环,而 foldl 因为从左边开始,所以完全可以随时截断。

实际上是因为 Haskell 的惰性求值的关系,foldl 是从左到右尾递归的,不需要中间展开栈空间,但是代价就是仅当递归结束以后才会返回一个值,对于无穷 list 的情况,不能返回一个惰性的 list。

foldr 不是,引用上了 foldr 并返回一个 list 的时候,实际上还没真正开始折叠就返回了一个惰性的 list,当去 take 的时候才会执行里面的代码。foldr 的时候,返回的 list 的头部变成了 x+1 但余部还没进行求值。

当然,foldl确实是从左到右折叠的,而 foldr 确实是惰性地从被截断处的右端到左端折叠的。

这里面还有限制就是你在传进 foldr 的函数中,只能不能获取余部的值:

foo :: Num a => a -> [a] -> [a]
foo v [] = [v]
foo v (x:xs) = v+x:(x:xs)

这样的函数

> foldr foo [] [1..10]
[55,54,52,49,45,40,34,27,19,10]
> foldr foo [] [1..]
...

就会陷入无限循环了。因为你要求了后面的值,所以就惰性不起来了,必须算完。

具体推荐这篇,特别是里面两张图非常有助于理解。

类型的 kind

类型的 kind 语法类似函数,实际上也类似函数:

* -> * -> *
a -> a -> a

对于函数来说,可以看作需要多少个值,才能确定一个最终的、非函数的值。

而对于 kind 来说,就是需要多少个类型,才能确定一个最终的类型。所以就像函数签名一样,kind 也在描述所需的类型参数的结构。

而高阶 kind,正如高阶函数需要别的函数来确定最终值一样,高阶 kind 也是需要别的多态类型(也就是说还没被确定下来的类型,kind 不是 *)来确定自己的类型,高阶函数的参数可以替换所以很灵活,高阶 kind 的类型构造器也可以替换,更是灵活,这也是后面很多 Type class 能被定义出来的基础。


今天要去拔牙了 QAQ