函数式编程
CouriourC Lv4

一句话简述

函数式函数式编程,是一种从数据流和数据关系的思考方式,可读性强且安全。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
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
// 定义一个减法
int minus(int a,int b){
return a-b;
}
// 定义一个加法
var add = (int a,int b)=>a+b;
// 将 minus 赋值给另外一个变量
var minusTwo = minus;
// 使用函数
print(add(1,2)); // 输出 3
print(minus_2(1,2)); // 输出 -1
}

  • 状态及可变性管理。
    函数式编程中的特点是,纯函数,所谓纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数。

这和数学中的关注点一样 f(data) = (other_state_data),也就是 dataIndataOut,这个之间只是来自于我们的函数作用不同,进一步请关联一下数学中基本的函数基础,单调性对称性复合函数、甚至是一些其他复杂的概念,这是函数的切入点数据的切入点则可以理解数据流,数据如同水流一样,在奔向大海的途中,有阻碍,有汇入,但是本质还是那一堆水的映射。所谓副作用,就是不污染全局,但是接受闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 全局的作用域
var globalVarity = 0;

dirty(){
// 污染全局变量
globalVarity++;
}
// 又一个函数调用
dirtyTwo(){
globalVarity++;
}
// 那么在并发的时候,Opps 你猜是什么,这还是微任务的情况下,是可预见的。
main(){
while ( globalVarity < 10) {
Future.delayed(Duration(seconds: 0),(){
dirty();
print('for 1 $globalVarity');
});
Future.delayed(Duration(seconds: 0),(){
dirtyTwo();
print('for 2 $globalVarity');
});
// 篡改
globalVarity++;
}
// 面目全非的 globalVarity;
}

函数式编程与其他范式的区别

面向过程编程

命令式编程,就是 one by one,这种思考方式偏向于原子化,经典的例子:把大象关进冰箱需要几步。面向对象的描述方式是说,第一步,打开冰箱,第二步装入冰箱,第三步关上冰箱。

面向对象编程

面向对象编程,将操作方式进行分类,让动作属于某一个对象,然后进行架构。依然以关大象举例,大象打开了冰箱(对象作用),大象走进了冰箱,大象关上了门。

声明式编程 (面向字典编程 🧠)

声明式编程,一切都已经安排好了,是一套预定的规则,比如 CSS,你只需要加上合适的属性,就能完成编程,底层已经为您做了实现。还是以上面关大象为例子,大象可以被装进冰箱,冰箱可以装大象。然后声明,大象在冰箱中。

函数式编程起源:λ 演算

λ 演算和图灵机等价(图灵完备,作为一种研究语言又很方便)。

所以 λ 演算式就三个要点:

  1. 绑定关系。变量任意性,x、y 和 z 都行,它仅仅是具体数据的代称。

  2. 递归定义。λ 项递归定义,M 可以是一个 λ 项。

  3. 替换归约。λ 项可应用,空格分隔表示对 M 应用 N,N 可以是一个 λ 项。

比如这样的演算式:


上面的演算式表示有一个函数 f 和一个参数 x。令 0 为 x,1 为 f x,2 为 f f x…,这就像我们数学中的幂:a^x(a 的 x 次幂表示 a 对自身乘 x 次)。相应的,我们理解上面的演算式就是数字 n 就是 f 对 x 作用的次数。有了这个数字的定义之后,我们就可以在这个基础上定义运算。

对应到我们前端使用的结果其实就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// 0 在函数中理解为不作为,就是什么都不做
const zero = f=>x=>x;
// 进入了一个层级
const one = f=>x=>f();

// 定义加法
const add = (n,m)=>f=>()=>m(f)(n(f)())
// 那么 2 就是
const two = add(one,one)

// 给2传一个函数,会打印2次:
two(function () {
console.log('print 2 times');
})();

// 计算数字3 = 1 + 2:
let three = add(one, two);
three(function () {
console.log('print 3 times');
})();

在函数式编程的世界中,一切皆函数,函数是一等公民。一等公民这个名字听起来很高大上,但是也相当晦涩,这个词也不是翻译的不好,因为英文原文中叫 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 主要特性如下:

  1. Ramda 强调更加纯粹的函数式风格。数据不变性和函数无副作用是其核心设计理念。这可以帮助你使用简洁、优雅的代码来完成工作。
  2. Ramda 函数本身都是自动柯里化的。这可以让你在只提供部分参数的情况下,轻松地在已有函数的基础上创建新函数。
  3. 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 或其他。

 评论