一句话简述
函数式函数式编程,是一种从数据流和数据关系的思考方式,可读性强且安全。Pay attention!!! 函数表示一切,包括数字。
约定
本文使用 Dart
语言描述,对于编程范式,具体是什么语言并不重要,只是顺手而已。
为什么需要函数式编程
可预测、Bug 少
函数式编程中,以函数为载体,数据流为驱动,数据是不允许篡改的,数据的操作是公开的,数据的流动是可预测的。这样就能很好的规避竞态问题。
易于测试
以函数为叙述单元,只需要知道数据的来源,对于数据的返回值是,正如之前所说,可预测的数据流,那么就很容易将用例拆分,从而做到测试。
移植性高
函数为思考主体,从而做到模块化。
函数式编程是什么
根据维基百科所述,函数式编程(又叫做函数程序设计、泛函编程、Functional Programing),是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序[状态]( https://zh.wikipedia.org/w/index.php?title=状态_ (%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)&action=edit&redlink=1)以及可变物件。从定义出发,有两个关键点:
- 函数运算。
在 函数式编程中,函数就是头等公民,你可以将函数名当作变量赋值给其他变量,使用方式不受影响。比如
1 | void main() { |
- 状态及可变性管理。
函数式编程中的特点是,纯函数,所谓纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数。
这和数学中的关注点一样 f(data) = (other_state_data)
,也就是 dataIn
,dataOut
,这个之间只是来自于我们的函数作用不同,进一步请关联一下数学中基本的函数基础,单调性
、对称性
、复合函数
、甚至是一些其他复杂的概念,这是函数的切入点。数据的切入点则可以理解数据流,数据如同水流一样,在奔向大海的途中,有阻碍,有汇入,但是本质还是那一堆水的映射。所谓副作用,就是不污染全局,但是接受闭包。
1 | // 全局的作用域 |
函数式编程与其他范式的区别
面向过程编程
命令式编程,就是 one by one
,这种思考方式偏向于原子化,经典的例子:把大象关进冰箱需要几步。面向对象的描述方式是说,第一步,打开冰箱,第二步装入冰箱,第三步关上冰箱。
面向对象编程
面向对象编程,将操作方式进行分类,让动作属于某一个对象,然后进行架构。依然以关大象举例,大象打开了冰箱(对象作用),大象走进了冰箱,大象关上了门。
声明式编程 (面向字典编程 🧠)
声明式编程,一切都已经安排好了,是一套预定的规则,比如 CSS
,你只需要加上合适的属性,就能完成编程,底层已经为您做了实现。还是以上面关大象为例子,大象可以被装进冰箱,冰箱可以装大象。然后声明,大象在冰箱中。
函数式编程起源:λ 演算
λ 演算和图灵机等价(图灵完备,作为一种研究语言又很方便)。
所以 λ 演算式就三个要点:
绑定关系。变量任意性,x、y 和 z 都行,它仅仅是具体数据的代称。
递归定义。λ 项递归定义,M 可以是一个 λ 项。
替换归约。λ 项可应用,空格分隔表示对 M 应用 N,N 可以是一个 λ 项。
比如这样的演算式:
上面的演算式表示有一个函数 f 和一个参数 x。令 0 为 x,1 为 f x,2 为 f f x…,这就像我们数学中的幂:a^x(a 的 x 次幂表示 a 对自身乘 x 次)。相应的,我们理解上面的演算式就是数字 n 就是 f 对 x 作用的次数。有了这个数字的定义之后,我们就可以在这个基础上定义运算。
对应到我们前端使用的结果其实就是
1 |
|
在函数式编程的世界中,一切皆函数,函数是一等公民。一等公民这个名字听起来很高大上,但是也相当晦涩,这个词也不是翻译的不好,因为英文原文中叫 first class citizen 很多人包括我也不知所云。其实所谓一等公民,它的意思是函数与基本数据类型一样,可作为函数的入参,也可作为函数的返回值,函数可以赋值给变量。我们知道在平常的命令式编程语言中(例如 Java)中,函数的返回值比较简单,只能是基本数据类型(整型,布尔,字符串等)或者是一个 Object。而在 JavaScript 函数是第一公民,因此我们也可以在函数中返回函数。正因为有了这种属性,函数的入参可以是函数,函数的返回值可以是函数,于是便有了高阶函数,以及各种骚操作和一些看起来很炫酷的语法糖。可以说函数为第一公民是函数式编程的必要条件。
前端中常见的 FP 概念
From Functional Programming Jargon(opens new window)
- Higher-Order Functions (HOF):高阶函数
- Closure:闭包(数学集合中的概念)
- Currying:柯里化
- Function Composition:函数组合
- Pure Function:纯函数
- Side effects:副作用
- Point-Free Style:隐式参数
- Functor:函子
- Lambda Calculus:Lambda 演算
- Lazy evaluation:惰性求值
Ramdajs
Ramda 主要特性如下:
- Ramda 强调更加纯粹的函数式风格。数据不变性和函数无副作用是其核心设计理念。这可以帮助你使用简洁、优雅的代码来完成工作。
- Ramda 函数本身都是自动柯里化的。这可以让你在只提供部分参数的情况下,轻松地在已有函数的基础上创建新函数。
- Ramda 函数参数的排列顺序更便于柯里化。要操作的数据通常在最后面。
Ramda 中有关 FP 概念的 API
- partial
- curry
- lift
- compose/pipe
RxJS
RxJS 是一个库,它通过使用 observable 序列来编写异步和基于事件的程序。它提供了一个核心类型 Observable,附属类型 (Observer、 Schedulers、 Subjects) 和受 [Array#extras] 启发的操作符 (map、filter、reduce、every, 等等),这些数组操作符可以把异步事件作为集合来处理。
在 RxJS 中用来解决异步事件管理的的基本概念是:
- Observable (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。
- Observer (观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。
- Subscription (订阅): 表示 Observable 的执行,主要用于取消 Observable 的执行。
- Operators (操作符): 采用函数式编程风格的纯函数 (pure function),使用像 map、filter、concat、flatMap 等这样的操作符来处理集合。
- Subject (主体): 相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。
- Schedulers (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeout 或 requestAnimationFrame 或其他。