Functional Programming¶
函数式编程概念¶
Functional Programming in JavaScript¶
Eric Elliott¶
- Functional programming is the process of building software by composing pure functions , avoiding shared state , mutable data , and side-effects. Functional programming is declarative rather than imperative , and application state flows through pure functions.
- Contrast with object oriented programming, where application state is usually shared and colocated with methods in objects.
- 函数式编程是通过 组合纯函数、避免 共享状态、 可变数据 和 副作用 来构建软件的过程。函数式编程是 声明性的而不是命令性的 ,应用程序状态通过纯函数流动。
- 相反,在面向对象编程中,应用程序状态通常由于对象中的方法来共享和协作。
关键概念¶
- Avoid side effects/避免副作用
- Avoid mutating state/避免改变状态
- Avoid shared state/避免共享状态
- Pure functions/纯函数
- Function composition/函数组合
- Declarative rather than imperative/声明式而非命令式
对比OOP¶
- 面向对象优点:对象的概念容易理解,方法调用灵活。
- 面向对象缺点:对象可在多个函数中共享状态、被修改,极有可能会产生“竞争”的情况(多处修改同一对象)。
- 函数式优点:避免变量的共享、修改,纯函数不产生副作用;声明式代码风格更易阅读,更易代码重组、复用。
- 函数式缺点:过度抽象,学习难度更大。
避免副作用,使用纯函数¶
Avoid Side Effects and Using Pure Functions
例子¶
- 副作用函数 vs 纯函数
// 副作用函数
let count = 1;
let incrememt = function() {
count++;
return count;
};
// 纯函数
let incrememt = function(count) {
count++;
return count;
};
纯函数(Pure Functions)¶
- 定义:当时函数被调用,它不会修改函数范围(Scope/作用域)外的任何东西。
- 特性1:该函数依赖于所提供的输入,而不是依赖于外部数据的变化。
- 特性2:该函数不引发副作用,不会引起超出其范围的变化。
- 特性3:给定相同的输入,函数总是返回相同的输出。
副作用(Side Effects)¶
- 改变一个全局值(变量、属性、数据结构)
- 改变函数参数的原始值
- 抛出异常(Throwing an exception)
- 打印控制台日志(Logging to the console)
- 写到屏幕或记录日志(Writing to the screen)
- 写到文件(Writing to a file)
- 写到网络(Writing to the network)
- 触发外部进程
- 调用一个有副作用的函数
副作用能消除吗¶
- 不能,相反,副作用必须发生。
- 我们需要副作用发生来达成目标,但滥用副作用导致问题。
- 函数式编程的目标只是为了更好的管理副作用,减少不必要的发生。
- 管理方法:将 有副作用的代码 集中隔离;无副作用的部分 被依赖,使代码更易测试、可推理、可阅读。
- 「变化是一只🐱,把猫关在笼子里」。
避免共享状态¶
Avoid Shared State
状态(State)¶
- A program is considered stateful if it is designed to remember data from events or user interactions.
- 如果程序旨在记住来自事件或用户交互的数据,则该程序被认为是 有状态的。
- 被记住的信息,称为程序的状态。
共享状态¶
- Shared state is any variable, object, or memory space that exists in a shared scope, or as the property of an object being passed between scopes. A shared scope can include global scope or closure scopes. -- Eric Elliott
- 共享状态 是存在于共享作用域中的任何变量、对象或内存空间,或作为作用域之间传递的对象的属性。共享作用域可以包括全局作用域或闭包作用域。
- In OOP, shared states are often passed around as objects.
- A shared state prevents a function from being pure.
避免改变状态¶
Avoid Mutable Data
可变的(Mutable)¶
- An immutable object is an object that can’t be modified after it’s created. Conversely, a mutable object is any object which can be modified after it’s created.
- 不可变对象是在创建后无法修改的对象。相反,可变对象是在创建后可以修改的任何对象。
不可变性(Immutability)¶
-
ES6 的
const
修饰符- 无法被重新赋值
- 但对象的属性可变
-
Object.freeze()
- 能 freeze 对象的顶层属性
'use strict'; const a = Object.freeze({ foo: 'Hello', bar: 'world', baz: '!' }); a.foo = 'Goodbye'; // Error: Cannot assign to read only property 'foo' of object Object
- 但无法 freeze 更深层次的属性依旧可变
'use strict'; const a = Object.freeze({ foo: { greeting: 'Hello' }, bar: 'world', baz: '!' }); a.foo.greeting = 'Goodbye'; console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
-
数据结构
trie
- 真正的 deep frozen
- 库
Immutable.js
和Mori
都使用 tries
// 使用 immutable.js 后 import Immutable from 'immutable'; const foo = Immutable.fromJS({ a: { b: 1 } }); const bar = foo.setIn(['a', 'b'], 2); // 使用 setIn 赋值 console.log(foo.getIn(['a', 'b'])); // 使用 getIn 取值,打印 1 console.log(foo === bar); // 打印 false
- 算法思想:如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享
克隆对象¶
Object.assign()
JSON.parse(JSON.stringify(obj))
使用 Reduce, Map 和 Filter¶
函数组合¶
Function composition
一个例子¶
- 题设:string -> trims 空格 -> splits 分词 -> check 长度 -> Boolean
- 解法:check(splits(trims(str)))
function trims(str) {
console.log('trims');
return str.replace(/\s/g, ' ');
}
function splits(str) {
console.log('splits');
return str.split('');
}
function check(arr) {
console.log('check');
return arr.length >= 3;
}
check(splits(trims('A C ')))
- 问题:嵌套多、执行顺序与阅读顺序相反;难以阅读、难以增加函数。
- 这代码咋读:
var a = b => c => d => "Wat?!";
Kyle Simpson¶
- 「You Don't Know JS」的作者
- ...readability is not just about fewer characters. Readability is actually most impacted by familiarity.
- 可读性不仅仅是更精简的代码。 可读性实际上更多与熟悉、熟练相关。
- ...A functional programmer sees every function in their program like a simple little Lego piece. They recognize the blue 2x2 brick at a glance, and know exactly how it works and what they can do with it. When they begin building a bigger, more complex Lego model, as they need each next piece, they already have an instinct for which of their many spare pieces to grab.
- 函数式程序员将程序中的每个函数都视为一个简单的乐高积木。他们一眼就能认出这块蓝色的2x2砖块,并确切地知道它是如何工作的,以及他们可以用它做什么。当他们开始建造一个更大、更复杂的乐高模型时,因为他们需要下一块积木,他们已经 有了一种本能,知道他们的许多备用积木中的哪一块。
compose 函数¶
function compose(...fns) {
return function(value) {
return fns.reduceRight((acc, fn) => {
return fn(acc)
}, value)
}
}
// 测试
compose(check, splits, trims)('A C ')
pipe 函数¶
function pipe(...fns) {
return function(value) {
return fns.reduce((acc, fn) => {
return fn(acc)
}, value)
}
}
pipe(trims, splits, check)('A C ')
科里化¶
Curry Function
思考题¶
- 多参数的 compose 函数怎么写?
- 提示:使用 bind 方法。(partial function)
function addition(x, y) {
return x + y;
}
const plus5 = addition.bind(null, 5)
plus5(10) // output -> 15
- 引出核心问题:Arity and How it Affects Compostion.
概念¶
- 参数个数(Arity)
- 科里化是 Arity 大于 1 的函数,变成多个 Arity 为 1 的函数,利用高阶函数和闭包。
- 单个参数的函数,称为「单元函数(unary function)」。
例子¶
- 多参数函数
function f1(a, b, c) {
return a + b + c;
}
function f2(d, e) {
return d + e;
}
function f3(f, g, h) {
return f + g + h;
}
- 科里化函数
function curry(fn, arity = fn.length) {
return function curried(...prevArgs) {
// 收集完参数退出
if (prevArgs.length >= arity) {
return fn(...prevArgs);
}
// 递归收集
return function(nextArg) {
return curried(...prevArgs, nextArg);
}
}
}
- 组合单元函数
const newFn = pipe(
curry(f1)(1)(2),
curry(f2)(4),
curry(f3)(5)(6),
)
newFn(3);
偏应用(Partial Application )¶
-
概念
- A partial application is a function which has been applied to some of its arguments, but not yet all of its argumens.
- 偏应用是一个函数,它应用于 某些参数,而非所有参数。
- In other words, it‘s a function which has some arguments fixed inside its closure scope.
- 换句话说,它是一个函数,在其闭包范围内固定了某些参数。
-
偏应用(Partial Application ) vs 科里化函数(Curried Function)
- Partial applications can take as many or as few arguments a time as desired.
- 偏应用一次可以根据需要使用多个或少个参数。
- Curried functions on the other hand always return a unary function:a function which takes one argument.
- Curried函数总是返回一元函数:接受一个参数的函数。
科里化优点¶
- 1、Currying can be used to specialize functions. 科里化能用于编写专业函数。比如, addUserAge10、movePointBy20。
const getUsersUser = pipe(
curry(getUser)(users),
cloneObject,
);
const getHery = function() {
return getUsersUser('Hery');
};
- 2.Currying simplifies function composition.科里化更易于完成函数组合。
声明式而非命令式¶
Declarative rather than imperative
关键概念¶
-
命令式编程(Imperative Programming)
- Imperative Programming is a programming style that tells the computer "how" to accomplish some task.
- 命令式编程是一种编程风格,它告诉计算机“如何”完成某些任务。
-
声明式编程(Declarative Programming)
- Declarative Programming expresses the logic of a program without identifying the control flow. Control flow is abstracted away, so declarative code only needs to specify "what" to do.
- 声明式编程在不识别控制流的情况下表达程序的逻辑。控制流被抽象出来了,所以声明式代码只需要指定 “做什么”。
- if条件、循环语句减少,不必知道“如何去做”,声明式代码更容易推理、可预测、可复用。
-
编程范式
命令式 vs 声明式¶
- 如图
-
看看代码 Code
- Code 1
let state = { foreground: '#999999', background: '#FFFFFF' }; const imperativeMakeBackgroundBlack = () => { state.background = '#000000'; }; // directly changes the state object outside of its internal scope const declarativeMakeBackgroundBlack = state => ({...state, background: '#000000'}); // takes current state as its input and returns new state with changed value // without changing the original state
- Code 2
let turtles = ['Galápagos tortoise', 'Greek Tortoise']; const imperativeAddTurtle = turtle => turtles.push(turtle); // changes the turtles external array and returns the length of the new array const declarativeAddTurtle = turtles => turtle => [...turtles, turtle]; // takes 'array of turtles' and the 'new turtle' as its input. // It returns new array of turtles without changing the original array
-
Imperative code directly accesses the state and changes it and declarative expressions never change the external state.
- 命令式代码直接访问状态并更改它,声明式表达式从不更改外部状态。
- 声明式更专注于 function composition
- 声明式以「intention revealing name」的方式编写函数代码,更容易阅读、推理、和理解。
工具库 Lodash & Ramda¶
Lodash¶
Ramda¶
- Ramda emphasizes a purer functional style. Immutability and side-effect free functions are at the heart of its design philosophy. This can help you get the job done with simple, elegant code.
- 1、Ramda 强调更加纯粹的函数式风格。数据不变性和函数无副作用是其核心设计理念。这可以帮助你使用简洁、优雅的代码来完成工作。
- Ramda functions are automatically curried. This allows you to easily build up new functions from old ones simply by not supplying the final parameters.
- 2、Ramda 函数本身都是自动柯里化的。这可以让你在只提供部分参数的情况下,轻松地在已有函数的基础上创建新函数。
- The parameters to Ramda functions are arranged to make it convenient for currying. The data to be operated on is generally supplied last.
- 3、Ramda 函数参数的排列顺序更便于柯里化。要操作的数据通常在最后面。
- 最后两点一起,使得将多个函数构建为简单的函数序列变得非常容易,每个函数对数据进行变换并将结果传递给下一个函数。Ramda 的设计能很好地支持这种风格的编程。
RxJS¶
ReactiveX¶
- 相关概念
RxJS¶
- 概念及定义:RxJS is a functional reactive programming library.
- 关键词:functional reactive programming、 异步
- 核心概念
- 样例代码