另一个函数式编程笔记 #
在阅读本文前,建议拥有一下知识:
基本的面向对象编程范式知识
听说过匿名函数
本文会从函数式独特的角度进行切入,介绍函数式的基本内容(通常是有利于 OOP 和其他编程范式的部分),并不提及「算子」等更为抽象的、来自 lambda 演算理论的术语。
函数式编程简称「FP」(Functional Programming)。
严格的函数式 #
从不变性开始的极端函数式生活
函数式有几个核心概念,也是其他编程范式主要吸收的地方。
不变性 Invariance #
不变应万变。
在传统的编程中,通常需要关心状态。
user.age = 25 // 修改了 user 对象的状态
if (user.age > 20) { // 访问 user 对象的状态
console.log(user);
}
可变的状态是造成程序难以理解和复杂多变、难以调试、难以并发的主要原因。
而在 FP 中,核心是「不变性」。即一切都是不变的,不存在变量,只有常量。
有人会问,那这样怎么进行「变化」呢?FP 的答案是「当你需要一个新的数据时,不是原地修改数据,而是创建一个新的数据」。
这么做会带来好处,因为没有数据修改,在多线程编程中多个线程就可以安全读取同一个数据。
纯函数 Pure Function #
函数式编程要有函数。
FP 中一个十分重要的概念是 纯函数,一个函数被称为纯函数需要满足以下两点:
输入决定输出:相同的输入总是返回相同的输出。
无副作用:不会修改作用域外的任何东西。
纯函数的概念接近于数学中的函数。
纯函数带来的好处在于几点:
简单测试:测试纯函数只需要关心输入输出,不用关心外部状态。
可缓存:相同的输入总是产生相同的输出,所以可以在计算后缓存结果,下次还有同样的输入可以直接返回缓存。
可预测:纯函数是独立的代码块,不依赖任何其他代码,一切逻辑都在他自身。
函数是一等公民 #
OOP 的话说就是:xx函数工厂,xx函数模式。
FP 中的函数可以作为参数,类似于数学概念「泛函」。
函数可以和普通的值一样当成参数传入或者返回值返回,在更宽泛的环境中(有变量的环境),函数可以当成变量的值。
在 FP 中,函数是一等公民的经典体现就是——以函数作为接收参数或返回值的函数称之为 高阶函数。例如经典的 sort() 通常能够传入一个比较大小的函数作为参数,这就是经典的高阶函数。
更松的函数式 #
抓到鼠鼠就是好哈基米。
严格的函数式(追求完全不变性)可能同样会让程序变得复杂和晦涩难懂。在许多关于函数式编程的应用中,反而是选择在面向对象和面向过程编程范式中局部地使用函数式或者使用函数式的思想会获得更多好处,例如简化程序。
下面有一些技巧,能够以函数式的方式重写传统 OOP 和 POP 代码,获得函数式的好处。
柯里化 Currying #
如同函数的包装。用一个高阶函数包装一个低阶函数。
如果要设计纯函数,通常会发现带有很多参数的纯函数,这时候可以选择用一个高阶函数来「保存」函数调用的上下文。例如下面这个例子:
function search(user_id, key) {
return users[user_id][key];
}
search("123","name");
search("123","age");
search("123","type");
可以将其拆成函数包函数的样子。
function query(user_id) {
return (key) => { // 这里是 javascript 的箭头函数语法,是一种匿名函数
return users[user_id][key];
}
}
function search(user_id, key) {
return query(user_id)(key); // 连续调用
}
// 调用
user_query = query("123"); // "123" 被「缓存」在匿名函数里了
user_query("name");
user_query("age");
search("123", "type"); // 经典的调用方式
这种将多参数的函数变换为单参数包装单参数函数的表示的技巧就是 柯里化 Currying。
柯里化的好处是无疑的:
能够灵活地创建函数:如上所示,可以对一个多参数的函数进行包装,缓存一部分参数从而轻松制造出新的函数。
延迟执行:在调用函数的时候无需知道所有参数,其他参数可以在稍晚的时候才知道。
闭包 Closure #
闭包特性简单易懂,但是十分强大。
闭包是一种语言上的特性:函数能够访问它创建时的作用域,即便它已经被移动到了其他地方。
在 OOP 里面,可能这个特性平平无奇,但是由于「函数是一等公民」的思想,很多语言也支持将函数作为变量传来传去,包括匿名函数。
最经典的关于闭包的就是这个例子:
function createCounter() {
let count = 0; // 这个变量在函数执行完后本应被销毁
return function() {
count++; // 内部函数访问并修改了外部函数的 count 变量
return count;
};
}
const counterA = createCounter(); // counterA 是返回的内部函数,它“记住”了属于它的 count
const counterB = createCounter(); // counterB 有自己独立的 count 变量
console.log(counterA()); // 输出 1
console.log(counterA()); // 输出 2 (count 状态被持久化了)
console.log(counterB()); // 输出 1 (互不影响)
上面的例子中,createCounter 函数返回了一个匿名函数,能够修改 createCounter 内的局部变量 count,达到在外面访问状态的效果。
神奇吧,明明没有 class,但是却似乎处处都是 class。
这么做有什么好处呢:
持久化状态:这是对不变性的背叛(因为用了变量),但是却让函数有了变换的可能。给了函数拥有私有状态的能力。
函数式的 OOP:以一种类似 OOP 又类似 FP 的方式进行了代码编写,能够和两者兼容而不会差异太大。