Zhihao's Studio.

浅谈函数式编程 (Introducing Functional Programming)

Word count: 2,801 / Reading time: 10 min
2018/07/30 Share

前言

最近两周利用空余时间艰难“啃完”了objc.io出版的《函数式Swift》这本书,感觉有些摸到了函数式编程的门道;在函数式编程思维的影响下,将之前的项目代码进行了改造。关于函数式编程,也算是有了一点心得,遂写成此文,虽然行文主要是以Swift为载体,但并不影响函数式思想的介绍。由于本人才疏学浅,而函数式编程本身博大精深,故谬误在所难免,如发现,还请指出。

《函数式Swift》

函数式编程

WHAT is 函数式编程

wiki对于函数式编程的定义如下:

In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It is a declarative programming paradigm, which means programming is done with expressions or declarations instead of statements.

我认为最重要的两个单词是programming Paradigmdeclarative,而后者又是为了描述前者准备的。什么是programming Paradigm?我在前几个月写的博客《面向协议编程初探》中已经解释过了,中文可以翻译为编程范式,我理解为编程语言设计者希望编程语言的使用者在使用编程语言的时候,思考问题的方式。

而declarative可以翻译为声明式的,与之相对应的是imperative(指令式的),目前最为广泛使用的面向对象编程就可以划到指令式编程这一类。(declarative和imperative的区别在下文中有所提及)

函数式编程的和面向对象编程的历史

在函数式编程面前,面向对象编程其实是晚辈。如果以smalltalk的出现作为面向对象编程元年,那么面向对象编程的历史应该从1975年算起(数据来源百度百科);而函数式编程的元年可以追溯到Lisp语言出现的1958年(数据来源百度百科)。

函数式编程被很多大佬美誉为the next big thing,甚至被称为最好的编程范式。让人不免疑惑,既然比面向对象编程出来的早,为什么之前没火,而现在又火了呢?

WHY函数式编程这两年又火了

带着疑问,我在搜索引擎中搜索了“函数式编程”和“火了”这两个关键词,找到了一个知乎问答,了解了这一段历史。

很赞成知乎用户狗好看的回答:

根本的原因是摩尔定律不适用。cpu的性能提升将体现在核数增加,这样并行的程序运行速度会越来越快。并行的程序的写法就是找出不能并行的地方,其他地方都尽量并行。如果要这样写,最需要避免的事情就是赋值。函数式编程的本质就是,规避掉“赋值”。

他的回答比较不容易懂,我来用我的理解翻译翻译

摩尔定律: 当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。

越来越多的人知道,放在初期还是成立的摩尔定律,最近有点不适用了。这和其他很多学科一样,开始的指数级发展,很容易让人过于乐观,到了瓶颈期后,学科发展很容易停滞不前。一个最明显的例子是医学领域关于癌症的笑话,说癌症被攻克,永远还需要30年。我用的第一台电脑的CPU是奔腾4的,同期经常听到的词还有赛扬处理器,奔四有1.4GHz左右的内核时钟,到今天我用的是2016年的顶配MacBook Pro,查了一下,参数为2.7GHz(约20年过去了,还不到2000年的两倍)。

2016年的顶配MacBook Pro

Intel的工程师也尝试过将这一参数加到3GHz甚至更高,但是他们发现功耗太高、发热太快。但是每年产品线又要更新,那怎么办呢?只能是往CPU里塞核心来获得计算能力吞吐量,刚刚发布的MacBook Pro 2018顶配已经用上了多达6核心的i9处理器,甚至连iPhone X都已经有了6个核心。将这些核心都利用上,可以让设备充分发挥作用,如果没有充分利用,很多核心就会在那里空转。为了充分利用多核心,在面向对象编程的世界中,经常用到的技术是同步机制和加锁,但由于函数式编程的特性,在函数式编程的世界里就不会出现这个问题,因此函数式编程又火了

其实在科技界这种死灰复燃的例子还有很多,zelear的王自如在他的节目《科技相对论》小众产品复活指南里曾经介绍过几款死灰复燃产品,比如:有轨电车、死飞、机械键盘、拍立得、车载广播等。这些产品和函数式编程一样,都没有被替代的那么彻底,时过境迁,找到了合适的土壤,用户突然又开始想念他的某个功能,所以又活过来了。

函数式编程的特性(HOW)

很遗憾,经过上一节的解释,我依然未能说清函数式编程是什么。这个问题跟面向对象等其他编程范式一样很难给出准确的定义,只能从几个比较热门的特性列举一些例子。

一等函数

函数的重要性在函数式编程里不言而喻,在支持函数式编程特性的语言里,函数被提到了一个非常重要的位置,他跟Int、String、Bool有着相同的地位。函数可以作为变量的字面量存储起来、作为函数的参数和返回值在函数之间传递。

函数作为变量的例子很多,只想说一句话:

闭包是函数的字面量,就像1之于Int,true之于Bool。

Currying

接下来举一个函数作为返回值的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func addFactory(value1 : Int) -> (Int -> Int){
func adder(value2 : Int) -> Int {
return value1+value2
}
return adder
}
let addOne = addFactory(1)
addOne(2) // return 3
addFactory(1)(2) // return 3
func add(value1 : Int , value2 : Int){
return value1 + value2
}

addFactory返回的值是一个函数,其类型为(Int -> Int),意思是返回值是一个接受Int并且返回Int的函数,我们可以用两种方式调用它。其中第二种addFactory(1)(2)又被称为我们习以为常的add(1,2)这种函数调用方式的Currying(柯里化)。多说一句,Currying是一个人的名字,他的全名叫Haskell Currying,剩下的应该不需要多解释了。

高阶函数

接受其它函数作为参数的函数有时被称为高阶函数。或许大多数人都使用过Map/Reduce/Filter,他们就是高阶函数最常见的例子。

1
2
3
4
5
6
7
8
9
10
11
12
let nums = [1,4,3,5]
// way 1
let strs : [String] = []
for i in (0..<nums.count) {
strs[i] = "No." + String(nums[i])
}
// way 2
let brr = nums.map {
"No." + String($0)
}

以Map为例,假设我们有这样一个需求,将一个整型数组,变为一个String类型的数组,并且在每个数字前加上“No.”,一个没有函数式编程思想的程序员极可能写出way1的代码,这种循环的代码几乎每个程序员都写了几千遍了,很好懂也并没有觉得有什么异常,但其实这是一种指令式的编程方式。何为指令式编程呢?就是人以机器的思维方式去思考,我们把自己当做了一台机器,比如上面way1的实现方式,就是我们将思维映射到了CPU上,强迫自己像CPU一样去思考。机器是怎么处理这个问题的呢?他首先要开辟一片内存,然后变更寄存器的值映射到变量i上,通过递增来做循环,然后创建字符串的字面量放到刚开辟出来的内存指定位置上。

way2使用了集合类型的高阶函数,它接收一个参数,这个参数是另一个函数(函数名不重要),负责String的初始化的方法。当它拿到这个函数之后,自动帮我们把里面的每一个元素拿出来,传到这个String的初始化函数里面去,就生成了最终的数组。这就是声明式编程,好处很明显,代码比以前短了很多,思维方式变得更像人思考的方式了。

有了高阶函数,函数可以自由装配,由一些简单的函数装配出一些高级的函数。

装配过程的可视化

关于函数组合的例子,《函数式Swift》给出的实例是对CoreImage库的使用,对一幅图像进行模糊、加滤镜、切圆角等操作。一些网红照片的出炉,通常也是由这些看似最简单的操作组合起来的。

纯函数(pure function)

在函数式编程中,对于函数还有两点特殊的要求。

  1. 不依赖外部
  2. 不改变外部
    满足上面两点要求的函数被称为纯函数(pure function)。这两点保证了无论在什么时候调用函数,对于相同的输入,总会得到相同的输出。这至少带来了两点好处:

1.函数的可测试性

2.上文提到的函数式编程没有的同步与加锁问题

至此也可以引出函数式编程的思想了:

避免使用程序状态和可变对象,是降低程序复杂度的有效方式之一,而这也正是函数式编程的精髓。函数式编程强调执行的结果,而非执行的过程。我们先构建一系列简单却具有一定功能的小函数,然后再将这些函数进行组装以实现完整的逻辑和复杂的运算,这是函数式编程的基本思想。

函子、适用函子、单子(Functor, Applicative, Monad)

这个部分需要单独写一篇文章介绍,我在理解过程中发现了一个很好的图解blog.

这里先给出结论:
functor: 通过 fmap 或者 <$> 应用是函数到封装过的值
applicative: 通过 <*> 或者 liftA 应用封装过的函数到封装过的值
monads: 通过 >>= 或者 liftM 应用会返回封装过的值的函数到封装过的值

参考

  1. 《函数式Swift》(https://objccn.io/products/functional-swift/)
  2. smallTalk 百度百科 (https://baike.baidu.com/item/smalltalk/1379989?fr=aladdin)
  3. Lisp 百度百科(https://baike.baidu.com/item/LISP/22083)
  4. 知乎:为什么函数式编程这两年又火了(https://www.zhihu.com/question/30190384/answer/142902047)
  5. 《科技相对论》小众产品复活指南(http://www.zealer.com/post/223.html)
  6. Functor, Applicative, 以及 Monad 的图片阐释 (http://jiyinyiyong.github.io/monads-in-pictures/)
CATALOG
  1. 1. 前言
  2. 2. 函数式编程
    1. 2.1. WHAT is 函数式编程
    2. 2.2. 函数式编程的和面向对象编程的历史
    3. 2.3. WHY函数式编程这两年又火了
  3. 3. 函数式编程的特性(HOW)
    1. 3.1. 一等函数
    2. 3.2. Currying
    3. 3.3. 高阶函数
    4. 3.4. 纯函数(pure function)
    5. 3.5. 函子、适用函子、单子(Functor, Applicative, Monad)
  4. 4. 参考