你也许不会在工作中用一门纯函数式编程语言(那样对大脑太不友好),但你应当学习这样一门语言,因为这将会改变你的思维,即使你平常使用的是一些常见的语言,如C++/Java(8+)/Python/JS,你还是能从这些语言中找到函数式编程的影子,另外,函数式编程思想在架构的设计上也有很大的作用。

What’s the difference?

函数式编程语言和我们平常使用比较多的命令式编程语言有极大的不同。

(像Haskell这样的)纯函数式编程语言和命令式编程语言有以下几个巨大的不同:

  1. 没有变量,一切量都是不变的
    • 因此也没有循环了
    • 也没有传统意义上的逻辑判断
    • 因此函数都是“纯函数”,因为没有变量,所以也几乎不可能有副作用3
  2. 函数是”一等公民”,每个函数都是一个变量,有其类型,能作为其他函数的参数2

看到这里你就意识到了,你需要把你在命令式编程语言界学到的大部分知识全部忘掉。

确实是这样,因为这完全是两种不同的思维体系,函数式编程和命令式编程的关系就像欧式几何和非欧几何一样。

你也许会以为没有变量是没有办法编程的1,但这是一种常见的误解,你将在下面看到,变量不仅没那么重要,而且有时“可变”只会带来麻烦。

Why Haskell?

Haskell除了是一门纯函数式编程语言外,还有如下特点

  1. 惰性求值,所有的值不被用到就不会被计算
  2. 静态强类型,并有着我所见过最好的类型系统

基本操作

基本数据类型

Haskell中的数据类型和C中的大致相同,整数字符浮点数应有尽有。

此外,Haskell中比较厉害的一个类型是列表类型,下面会说。

调用函数

Haskell调用函数的方式和C语言不太一样:

1
2
3
-- 假设有函数f
f x -- 这就是计算f(x)了
g x y -- g(x,y)

Haskell省去了括号和逗号,这虽然让人不太适应,但也部分避免了像Lisp那样一屏幕的括号的尴尬。

基本运算

+-*/&&||==都和C语言一样。

特别的,取反要用not(实际上这是个函数,在Haskell中这些运算符本质都是函数),C中的不等于符号!=,在这里是/=

求余要用mod函数:

1
mod 3 2

如果你想要像C语言中的%那样中缀调用mod的话,你可以使用` 将函数包裹:

1
3 `mod` 2

列表

首先要提一句,Haskell中所谓的字符串还是字符列表的一个语法糖。

Haskell的列表是一个非常强大的玩意,我们可以用和Python中制作列表相似的方法做一个Haskell列表:

1
[1,2,3,4,5,6]

Haskell列表可以被拼接:

1
2
[1,2]++[3,4]++[5,6]
-- [1,2,3,4,5,6]

也可以拼一个元素上去:

1
2
3
4
1:[1,2,3]
-- [1,1,2,3]
-- 注意只能拼在前面
-- [1,2,3]:1 会报错的

可以各种取元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
take 3 [1,2,3,4,5,6,7]
-- 取前三个,[1,2,3]
drop 3 [1,2,3,4,5,6,7]
-- 取前三个之外的元素,[4,5,6,7]
head [1,2,3,4,5,6,7]
-- 取第一个,1
tail [1,2,3,4,5,6,7]
-- 取除第一个之外的元素,[2,3,4,5,6,7]
init [1,2,3,4,5,6,7]
-- 取最后一个之外的元素,[1,2,3,4,5,6]
last [1,2,3,4,5,6,7]
-- 最后一个,7
1 `elem` [1,2,3]
elem 1 [1,2,3]
-- 判断元素是否在列表中,两句等价,都为True
[1,2,3,4] !! 2
-- 取列表的第二个元素,3
-- 用!!有点奇怪不是吗😓

另外构造列表还有很多特别的姿势:

1
2
3
4
5
6
7
8
9
-- 使用区间
[1..5]
-- [1,2,3,4,5]
-- 一般的等差数列都能推出来
[1,3..42]
-- [1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41]
-- 使用列表推导式
[x*2 | x <- [1..10], x*2 >= 12]
-- 意为:对于在[1..10]之中的x,若有x*2 >= 12,则将x*2放入列表

由于Haskell的惰性求值特性,你可以构造一个无穷的列表:

1
2
[1..]
-- 从1开始,到无穷大为止

运用这个特点我们能做一些很酷的事情,比如求前10个7的倍数或末尾含7的数:

1
take 10 [x | x <- [1..], x `mod` 7 ==0 || x `mod` 10 == 7]

就只有一行代码,简单方便,可读性好。

你拿命令式语言写,估计得絮絮叨叨写一大坨了吧。

自己写函数

Haskell的函数语法非常直白,很像数学中的函数:

1
triple x = 3 * x

就定义好了一个函数triple,它的作用就是返回输入参数的三倍。

很像数学里的函数的写法:

多个参数:

1
length x y = sqrt (x * x + y * y)

也很像数学中的函数:

如果函数要分类讨论,可以使用“模式匹配”等技巧,它们在函数式编程中替代了逻辑判断:

1
2
3
4
5
6
-- 当参数恰好匹配的时候会返回对应的值
sillyFunction 0 = 0
sillyFunction -1 = 2
sillyFunction 2 = 3
-- 都没有匹配到,会进入这个默认的匹配
sillyFunction x = x/2+1

就像是:

记得匹配的顺序是从上到下,因此如果参数为x的匹配放到第一个那么就会GG。

(说起来有点像匹配URL啊……)

如果要匹配的是一个范围,那么应当使用“哨卫”语法:

1
2
3
4
5
f x
| x <= 10 = x
| x <= 20 = x/2
| x <= 30 = x*x
| otherwise = x*x*x

就像是:

如果在列表上进行模式匹配,可以这样写:

1
2
3
4
5
listFunction [] = "Empty!"
listFunction (x:[]) = "Only One!"
listFunction (x:y:[]) = "There are two!"
-- 这里要提一下,show函数返回输入值的字符串表示
listFunction (x:y:_) = "More than 2! First two are " ++ show x ++ " and " ++ show y

递归

想要理解递归,你要先理解递归。

有了上面那一堆东西,大部分命令式语言能实现的东西就能被实现了,但是我们还没讲到一样重要的东西:递归。

我们来考察“最大值”函数,它应该接受一个列表,返回列表中的最大值。

在命令式编程中,我们会使用一个循环来实现这一点。

然而我们没有循环了,该怎么办?

用递归啊。

我们可以这样想,如果一个列表里只有一个元素,那么这个元素就是最大的:

1
maxInList [x] = x

否则,就应该是这个列表第一个值和其余部分中的最大值中比较大的那个

1
maxInList (x:xs) = max x (maxInList xs)

于是就这么愉快地写好了,这个函数只有两行,如果使用命令式语言,恐怕很难用两行完成(当然你要是硬要用C语言然后把代码都挤在一行上我也没话说)。

为了进一步展示函数式编程的美,我们来看看函数式的快速排序:

1
2
quicksort [] = []
quicksort (x:xs) = (quicksort [y | y <- xs, y < x]) ++ [x] ++ (quicksort [y | y <- xs, y >= x])

va,还是只有两行,爽!

进阶

curry化函数与Hindley-Milner类型签名

我们前面说过Haskell中的函数可以带多个参数:

1
length x y = sqrt (x * x + y * y)

但是我实际上,我要说,所有的Haskell函数都只接受一个参数,返回一个值。

那上面那个玩意是怎么弄出来的呢?

我们先看看这个函数的类型:(在GHCI中使用:t length

1
length :: Floating a => a -> a -> a

WTF?这是啥神仙玩意?

实际上这是一个叫Hindley-Milner类型签名的东西,Haskell主要使用这种东西来标记一个函数的类型。

这个东西这样读:

Length is a fuction which takes an argument of type “a” and returns a fuction which (

takes an argument of type “a” and returns a value of type “a”

) where “a” is a type of typeclass Floating

用中文:

Length函数接受一个”a”类型的值作为参数,返回一个(接受一个”a”类型值作为参数,返回一个”a”类型值的函数),其中”a”是Floating类型类下的类型。

如果你还是觉得有点晕的话,我们给上面的类型打上括号:

1
length :: Floating a => (a -> (a -> (a)))

可以理解为,这里一共有两个函数,一个是length本身,它接受一个”a”类型参数,返回一个类型为

1
Floating a => a -> a

的函数。

这个新的函数接受一个”a”类型参数,返回一个”a”类型的值。

所以一个Haskell函数只接受一个参数,然后要么返回一个函数,负责“吃掉”剩下的参数,要么返回一个值,就是函数运行的结果。

那么这样有什么好处呢?

一个好处是容易创建偏函数:

1
2
3
4
length x y = sqrt (x * x + y * y)
f = length 2
f 3 -- 即length 2 3
f 4 -- 即length 2 4

typeclass

看到typeclass不要想到面向对象中的class,相比之下,typeclass更像interface(尤其像Go的interface),也就是表达“一种类型的能力”(interface大概是这个意思)而非“一个对象的能力”(class大概是这个意思)。

说的简单一些,typeclass就要求一个类型能被放在某个函数的参数里做运算。

有以下一些常见的typeclass

  • Eq类型类

    可判断相等性的类型,要求类型实现了==/=两个函数4

  • Ord类型类

    可比较大小的类型,要求类型实现了compare

  • Show类型类

    可以转成字符串,也就可以被显示出来的类型,实现show

  • Read类型类

    可以从字符转出来的类型,实现read

  • Enum类型类

    可以求其前驱和后继的类型,实现predsucc

  • Bounded类型类

    有界的类型,实现minBoundmaxBound

  • Num类型类

    表示数值的类型类,基本上就是IntIntegerFloatDouble

  • Floating类型类

    表示浮点数的类型类,基本上就是FloatDouble

  • Integeral类型类

    表示整数的类型类,基本上就是Int(会溢出的整数)和Integer(大整数)。

这些就是一些基本的类型类。

当然你可以自己做一些类型,并让它们实现某个类型类,但这部分绝非Haskell最令人感到舒适的部分,毕竟这是传统面向对象语言的专长(而我的思维方式不幸地已经适应了这种写法)。

不过在此还是留下例子:

1
2
3
4
5
6
7
data TrafficLight = Red | Yellow | Green

instance Eq TrafficLight where
   Red == Red = True
   Green == Green = True
   Yellow == Yellow = True
   _ == _ = False

呓语

函子

CD为范畴,从CD函子为一映射F:

  • 将每个对象X \in C映射至一对象F(X) \in D上,
  • 将每个态射f:X\rightarrow Y \in C映射至一态射F(f):F(X) \rightarrow F(Y) \in D上,使之满足下列条件:
  • 对任何对象X \in \mathcal{C},恒有{\displaystyle F(\mathbf {id} _{X})=\mathbf {id} _{F(X)}}
  • 对任何态射f: X \to Y, \; g: Y \to Z,恒有F(g \circ f) = F(g) \circ F(f)。换言之,函子会保持单位态射与态射的复合。

一个由一范畴映射至其自身的函子称之为“自函子”。

上面这坨都是啥神仙玩意。

数学家就喜欢把其实很简单事情搞得看上去超级复杂,美其名曰“严密”,以凸显其远超常人的智商,实际上我们都知道……好吧他们是真的很聪明TAT。

我用一句话说清楚:

函子(Functor)就是可以被map-over(即通过map向对象中的子对象应用一个函数)的对象。5

或者,一码胜千言:

1
2
class Functor f where
  fmap :: (a -> b) -> f a -> f b

啊,Functor是一个类型类,它要求实现了它的类型实现fmap函数,它取一个(a -> b)和一个f a(即f类型里面的a类型)值作为参数,返回一个f b的值。

比如列表就是一个Functor

1
2
3
4
map :: (a -> b) -> [a] -> [b]

instance Functor [] where
  fmap = map

另外,Haskell中的SetMaybe(可空值)也是Functor

Maybe是个好东西,下面就用它讲解了:

1
2
3
instance Functor Maybe where
fmap func (Just x) = Just (func x) -- 有东西写作 Just xxx, map上去就是Just f(xxx)
fmap func Nothing = Nothing -- 没东西写作 Nothing, map上去还是Nothing

总的来说,Functor就是表现的像是容器或者上下文的一个类型,你可以通过fmap向容器中的元素应用一个操作这样。

Applicative

ApplicativeFunctor的升级版本(也称Applicative Functor),这是个啥呢?

我们已经知道了我们可以将一个函数map到一个Functor上,但是如果我们要应用的函数6也在上下文中呢?

例如Just (+3)这种?

在已经能完成Functor所有功能的基础上,Applicative也会帮我们解开函数的上下文,然后应用:

1
2
3
class Functor f => Applicative f where  -- Applicative一定是Fuctor
pure :: a -> f a -- 返回一个包裹在上下文中的值
(<*>) :: f (a -> b) -> f a -> f b -- 应用上下文中的函数

例如Maybe

1
2
3
4
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing -- 应用 Nothing 得 Nothing
(Just func) <*> something = fmap func something

Monad (Ah! Finally!)

Monad有啥难的,不过是自函子范畴上的一个幺半群罢了。

这么说的都给我拖出去毙了。就你懂群论代数系统范畴论。😠

抱歉毙不掉,第一个说这句话的是Haskell委员会成员。

MonadApplicative的升级版本,它在Applicative的基础上,添加了一个“接受一个上下文中的值和一个(接受普通值返回上下文中的值的函数),返回一个上下文中的值”的功能。

(简化过的)Monad是这样的:

1
2
3
class Applicative m => Monad m where    
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

同样看Maybe

1
2
3
class Applicative Maybe => Monad Maybe where
return :: a -> Maybe a
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b

那么引入这样一堆玩意有啥好处?

我们来看这个:

1
2
3
half x = if even x
then Just (x `div` 2)
else Nothing
1
2
3
4
5
6
Prelude> Just 20 >>= half						-- 把Just 20塞进half函数里
Just 10
Prelude> Just 20 >>= half >>= half -- 把Just 20的结果塞进half函数里
Just 5
Prelude> Just 20 >>= half >>= half >>= half
Nothing

链式调用,管道操作,酷毙了。

monad_chain

从函数式编程到函数式架构

上面这些东西都很不错,但我觉得你很难在“搬砖”的时候用到他们。

搬砖的时候最多用一下mapreducefilter和少量递归的想法,几乎没有可能会显式地用到ApplicativeMonad这些,原因很简单:他们太难了,很多人理解不能。

但是这些都是微观的函数式编程,我认为函数式真正NB的用途在于架构上,也就是“宏观的函数式编程”。

我们想一想我们在函数式编程里学到了什么。

  1. “可变的”、“副作用”是不好的
  2. 数据流>>函数>>函数>>函数 = 程序的结果

这些想法在架构中也能用到。

  1. “可变”、“副作用”会带来管理上的复杂性,每个函数或方法必须明确“调用前要满足的条件”和“调用后会导致的副作用”,而这很可能会导致“认知超载”,故应当限制可变的东西。

    或者说,可以将系统设计为:

    系统状态=f(系统状态,用户行为)

    这样系统状态的改变的唯一原因就是“用户行为”8

  2. 领域层(这是从DDD里借来的词) >> 渲染函数 = 表现层

将他们结合起来,我们可以得到这样一个架构:

  1. 有一个“领域层”
  2. 领域层 = applyAction(领域层,用户行为)
  3. 表现层 = render(领域层)

那么这样一个架构像什么呢?

你可以说它像MVVM:

  • “领域层” —— VM层
  • render函数——由MVVM框架负责提供。
  • 用户行为——是指用户修改了VM层的数据

但我认为它更像是Flux:

img

  • “领域层”——Store
  • render函数——自己提供
  • Action——就是Action

相比MVVM,Dispatcher为Flux提供了一个应用Action的统一入口,引起Store变化的原因被放在Dispatcher里面统一管理了起来,显得更加清晰。

1. 我记得lq老师的书中极言变量的重要性,我并不是说他说得不对,但我认为他只看到了Part of the story.
2. 做到这一点的语言就可以说是支持函数式编程, JS和Python本来如此,C++的支持通过函数指针/Callable object/lambda部分实现,Java 8+通过lambda部分实现。
3. 不幸的是,“副作用”还包括IO等,因此实际上是没有办法完全消去副作用的。不过,Haskell将IO限制在程序中一些特定的部分(do块等)。
4. 别忘了!Haskell 中所有运算符都是函数!
5. 注意这是Haskell和大部分程序语言中对Functor的定义,它并不完全和上面的严密数学定义等价!
6. 别忘了!函数是一等公民!它也是一个值,也能放在容器中!
7. FunctorApplicativeMonad的部分介绍和图片来自这里
8. 记得吗,一个类或者模块应该有且只有一个改变的原因(SRP),开发过程和程序的运行过程中的最佳实践其实也是相通的啊。
10. 当然,MVVM和Flux各个层之间的通信要靠各种不同的方法,比如Rx等。