为什么学 Haskell

就像序里面说的,为什么要学习 Haskell?当然是因为有趣呀(插图实在是太魔性了

之前看了本《Javascript 函数式编程》, 才刚刚接触到函数的世界,虽说感觉好像打开了新世界的大门, 但同时也多了很多困惑。问题足够小的时候,写几个纯函数,做做集合操作,感觉还是很顺畅,但一接触 到 dom,浏览器事件之类的时候,就有种无所适从的感觉。外部的状态太多了,保持函数的纯度好像是个 不可能的任务。而且,对如何用函数式的思维构建一个系统基本上没有什么概念。为了能更多地了解一点 函数式的世界,就找了这本书来看,果然没让我失望。

Haskell 是啥

Haskell 是一种纯函数式编程语言(purely functional programming language)。有多纯呢,这语言居 然连变量都没有……不过也对,如果都是纯函数,为啥还需要变量呢,只要常量就可以了。当然作为函 数式语言,引用透明、惰性求值等特性也是标配。

循环 =》 递归

要抛弃变量,首先想到的就是 for 循环了,i 可能是用的最多的一个变量名了。而解决循环,当然是用 递归了。从命令式到函数式最大的转变是,思考的方式要从描述『怎么做』转到描述『是什么』。这可能 也是从循环到递归最大的变化了吧,毕竟写起来差别并不大。

高阶函数

高阶函数指,函数可以接受函数作为参数,也可以将函数作为返回值返回。JS 也有着同样的特性,但 Haskell 从语言机制上做了更彻底的支持。最明显的一点是,Haskell 的所有函数都只有一个参数…… 因为 Haskell 是默认柯里化的(也太彻底了吧)。柯里化使得函数可以非常方便地获得一个『部分应用』 让函数的使用方便不少。 Haskell 也提供了很多机制,如函数组合(.),函数应用符($)等,写起来也是简洁很多,不像 JS,多层的 函数嵌套很容易破坏代码的可阅读性,一堆的园括号数都数不清(Haskell 函数参数不需要括号包裹)

类型与类型类

类型类这个名字听起来就挺拗口的,理解起来倒没那么难。 和面向对象编程不同,函数式的语言里面是没有对象和类的概念的,毕竟连变量都没有,对象的状态也不 可能存在了。但编程的时候总是会有要表达特定的结构的时候,Haskell 提供自定义类型来解决这个问题, 而且自定义类型能力非常强,它甚至可以是递归的(Haskell 中数组就是一个递归的类型)。 而对于不同类型之间通用的逻辑,Haskell 用类型类来代替面向对象编程中的继承机制(这么说可能有些 不恰当,这是完全不同的两种东西)。类型类,并不是 OO 中一个类的概念,非要说的话更像是一种接口 或者协议的概念。类型类规定一些行为,而拥有这些行为的类,就能成为这个类型类的实例。比如 Eq 类 型类表示可以判断是否相等的类型,Ord 表示可以排序的类型,show 表示可以打印的类型……

Functor

Functor 是一个类型类,表示可以映射的事物。它规定了实例必须实现 fmap 方法,fmap 接受一个参数 类型与返回值类型不同的函数和一个应用到某类型的函子(functor)值作为参数返回一个应用到另一个类型 的函子值…… 没关系,我第一次看到这句话的时候反复捋了三四遍才看明白。看一下 fmap 的类型定义可能会好理解一些

fmap :: (a -> b) -> f a -> fb

其中,a 和 b 就是两个不同的类型。a -> b 表示接收 a 作为参数返回 b 的一个函数。而 f 是一个只接 收单个参数的类型构造器,f a 就构造了一个类型。 举个具体的类型 f 可能会更直观一些。比如说我们把 f 定位数组,也就是 []。[] 是一个类型构造器, (在Haskell 中,数组只能储存相同类型的值,所以[String] 和 [Int] 是不同的类型)。这时候我们再看 一下数组的 map 方法

map :: (a -> b) -> [a] -> [b]

是不是感觉非常像,没错,数组的 fmap 实现就是 map。可以理解为 f 是包裹着值的上下文,通过 fmap 可以把一个直接作用在值上的函数,用在这个上下文上,返回的结果仍然处在这个上下文中。

纯度问题

纯粹与非纯粹的分离,这是之前一直困扰我的问题之一。如果一个语言完全没有副作用,什么都不改变,那 似乎就失去了意义,毕竟它什么都影响不了。

Haskell 提供了 IO 类型来解决输入输出问题。IO 类型也是一种上下文,一个容器,里面装着我们我们须要 的数 据,我们只能从中取出而不能直接操作。通过这种方法,外界不纯的因素跟程序中主要的逻辑就分开 了。在 JS 里面,这个问题感觉就复杂得多了,毕竟 DOM 就是一个充满各种状态的东西,不过也算提供了一 种不错的思路。

高纯度的另一个问题,就是随机性。既然相同的输入都会得到相同的结果,那怎么得到随机数呢。Haskell 提 供了随机性生成器和随机源来解决这个问题,用起来稍微麻烦一些,但也是无伤大雅。

Functor 到 applicative 到 Monoid

Applicative 是对 Functor 的升级。Functor 解决了如把 * 2 应用到 [1, 2] 从而得到 [2, 4] 的问题。但 如果我们想把 [( * 2 ), ( + 100 )] 应用到 [1, 2] 上,获得 [1, 2, 101, 102] 的时候,fmap 就有点力不 从心了。 applicative 可以看作是带有上下文的值,比如 ‘a’ 是一个普通的字符,类型是 Char,而 [‘a’] 带 有一个上下文,类型变成了 [Char],而 applicative 让我们可以把普通的函数包裹起来,用来处理这些带有 上下文的值,同时维持上下文的不变。 而 monad 是对 applicative 函子概念的延伸,解决的是这样的问题,我有一个带有上下文的值 m a,怎么把 a -> mb 这样的函数应用到它上面,也就是下面这个方法

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

虽然看起来都差不多,只不过多裹了几层,说得这么玄乎好像也没啥用,但正是这些类型类,带给了 Haskell 极大的灵活性。

monad 让自定义类型更容易处理,也提供了带状态计算的一种优雅的解决方案(State monad)。更多的比如 zipper 之类的东西,还是推荐看一下这本书。

Summary

书不算厚,但是断断续续看了几星期才看完,而且也不敢说自己看懂了多少,只能说自己还是不习惯这种思维方 式,想起来脑子经常打结。 但 Haskell 真的挺有趣的,并不只因为它魔性的插图。至于在生产环境的使用…. 管他呢,有趣不就够了嘛。