函数式编程概念
函数式编程(Functional Programming, FP)是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。
它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。 就是对运算过程进行抽象,函数式编程中函数指的不是程序中的函数(方法),而是数学中的函数即映射关系。
// 非函数式编程
let num1 = 1
let num2 = 2
let sum = num1 + num2
console.log(sum)
// 函数式
function add (n1, n2) {
return n1 + n2
}
let sum = add(1, 2)
console.log(sum)
函数式编程的特性
-
函数是“第一等公民(first class)”
"第一等公民"指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。- 函数可以赋值给其他变量
const fn = () => { console.log('我可以存储在变量中') } fn() // 示例 const obj = { index (posts) { return Views.index(posts) } } // 优化 const obj = { index: Views.index }
-
高阶函数(Higher-order function)
什么是高阶函数
- 可以把函数作为参数传递给另一个函数
// forEach const forEach = (array, fn) => { for (let i = 0; i < array.length; i ++) { fn(array[i]) } } let arr = [1, 2, 3, 4, 5] forEach(arr, i => { console.log(i) })
- 可以把函数作为另一个函数的返回结果
const once = fn => { let flag = false // 注意:这里返回的函数如果写成箭头函数的话,会找不到 this return function () { if (!flag) { flag = true return fn.apply(this, arguments) } } } const pay = once(money => { console.log(`支付了${money}元`) }) pay(5) pay(5) pay(5)
使用高阶函数的意义
- 抽象可以帮我们屏蔽细节,只需要关注我们的目标
- 高阶函数是用来抽象通用的问题
-
闭包(Closure)
函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包-
可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
-
本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
// 案例 function makePower (power) { return function (number) { return Math.pow(number, power) } } // 优化写法 const makePower = power => num => Math.pow(num, power) let power2 = makePower(2) // 求平方 let power3 = makePower(3) // 求立方 console.log(power2(2)) console.log(power2(3)) console.log(power3(2))
-
-
纯函数
- 相同的输入永远会得到相同的输出, 而且没有任何可观察的副作用
let arr = [1, 2, 3, 4, 5] // 纯函数 console.log(arr.slice(0, 3)) // [1, 2, 3] console.log(arr.slice(0, 3)) // [1, 2, 3] // 不纯函数 console.log(arr.splice(0, 3)) // [1, 2, 3] console.log(arr.splice(0, 3)) // [4, 5] console.log(arr.splice(0, 3)) // []
-
惰性计算
在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。 -
不修改状态
函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。 -
没有"副作用"
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
因为 FP 语言不包含任何赋值语句,变量值一旦被指派就永远不会改变。而且,调用函数只会计算出结果 ── 不会出现其他效果。
函数式编程的优点
- 代码简洁,开发快速
- 接近自然语言,易于理解
- 更方便代码管理
- 易于“并发编程”
- 代码的热升级
函数式编程基础
lodash
lodash(中文官网、英文官网)是一个一致性、模块化、高性能的 JavaScript 实用工具库
- 安装
npm install lodash
- 引入
const _ = require('lodash')
- 使用
const array = ['red', 'green', 'yellow', 'pink']
console.log(_.first(array)) // red
console.log(_.last(array)) // pink
console.log(_.toUpper(_.first(array))) // RED
console.log(_.reverse(array) // ['pink', 'yellow', 'green', 'red']
_.each(array, (v, i) => {
console.log(v, i) // red 0 green 1 yellow 2 pink 3
})
纯函数
- 可缓存
- 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
// 记忆函数 const _ = require('lodash') function getArea (r) { console.log(r, '我只会执行一次,因为我被缓存起来了') return Math.PI * r * r } let getAreaWithMemory = _.memoize(getArea) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) // 模拟实现 function myMemoize(fn) { let cache = {} return function () { let key = JSON.stringify(arguments) cache[key] = cache[key] || fn.apply(fn, arguments) return cache[key] } }
- 可测试
- 纯函数可以让测试更方便
- 并行处理
- 在多线程环境下,并行操作共享的内存数据很可能会出现意外情况
- 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(
Web Worker
)
函数柯里化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
// 简单的举例
const add = (a, b) => a + b
// Currying 后
const curryingAdd = a => b => a + b
add(1, 2) // 3
curryingAdd(1)(2) // 3
由此可以看出,柯里化一个函数,就是把原可以接收多个参数的函数变换成只接收单一参数,并用返回的函数接收余下的参数且返回结果的技术。
// 例:判断年龄基准值
function checkAge (min, age) {
return age >= min
}
// Currying 后
function checkAge (min) {
return function (age) {
return age >= min
}
}
// ES6 写法
let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
checkAge18(16) // false
checkAge18(20) // true
checkAge20(18) // false
checkAge20(22) // true
// 模拟实现 lodash 中的 curry 方法 (传入一个函数,把该函数转换为柯里化函数)
const curry = fn => {
return function curriedFn(...args) {
if (args.length < fn.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return fn(...args)
}
}
const getSum = (a, b, c) => (a + b + c)
const curried = curry(getSum)
curried(1, 2, 3) // 6
curried(1, 2)(3) // 6
curried(1)(2, 3) // 6
curried(1)(2)(3) // 6
总结
-
柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
-
这是一种对函数参数的‘缓存’
-
让函数变得更灵活,让函数的粒度更小
-
可以把多元函数转换成一元函数,可以组合使用,产生强大的功能
面试题:
// 实现一个add方法,使计算结果能够满足如下预期:
// add(1)(2)(3) = 6;
// add(1, 2, 3)(4) = 10;
// add(1)(2)(3)(4)(5) = 15;
const add = (...rest) => {
const fn = (...args) => {
rest.push(...args)
return fn
}
fn.toString = () => rest.reduce((total, num) => total + num, 0)
return fn
}
console.log(+add(1)(2)(3)) // 6
console.log(+add(1, 2, 3)(4)) // 10
console.log(+add(1)(2)(3)(4)(5)) // 15
组合函数
-
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间的过程函数合并成一个函数
-
函数组合默认是从右到左执行
// 实现一个组合函数
function compose (...args) {
return function (value) {
return args.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
}
// ES6改造一下
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
// 测试
const fn1 = arr => arr.reverse()
const fn2 = arr => arr[0]
const fn3 = str => str.toUpperCase()
const f = compose(fn3, fn2, fn1)
// const f = compose(compose(fn3, fn2), fn1)
// const f = compose(fn3, compose(fn2, fn1))
console.log(f(['one', 'two', 'three'])) // THREE
- 函数组合要满足结合律(associativity)
我们既可以把 a
和 b
组合,还可以把 b
和 c
组合,结果都是一样的
// 结合律
let f = compose(a, b, c)
compose(compose(a, b), c) == compose(a, compose(b, c))
// true
- 组合函数的调试
const _ = require('lodash')
// 需要写一个辅助函数做调试
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
// 将此辅助函数插入到组合函数中即可
compose(a, trace('我在b之后'), b, trace('我在c之后'), c)
Point Free模式
我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
案例演示
// Hello World => hello_world
// 非 Point Free 模式
function f (str) {
return str.toLowerCase().replace(/\s+/g, '_')
}
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World')) // hello_world
函子(Functor)
什么是Functor
?
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有
map
方法,map
方法可以i运行一个函数对值进行处理(变形关系)
总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了
map
契约的对象 - 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的
map
方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理 - 最终
map
方法返回一个包含新值的盒子(函子)
Functor
class Container {
constructor (value) {
this._value = value
}
map (fn) {
return new Container(fn(this._value))
}
}
new Container(5)
.map(v => v + 1) // Container { _value: 6 }
.map(v => v * v) // Container { _value: 36 }
// ============改============造============>
class Container {
static of (value) {
return new Container(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Container.of(fn(this._value))
}
}
Container.of(5)
.map(v => v + 1) // Container { _value: 6 }
.map(v => v * v) // Container { _value: 36 }
MayBe
处理空值情况
class MayBe {
static of(value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this.isNothing() ? MayBe.of(this._value) : MayBe.of(fn(this._value))
}
isNothing () {
return this._value === null || this._value === undefined
}
}
MayBe.of(null)
.map(v => v * v) // MayBe { _value: null }
Either
- Either 两者中的任何一个,类似与 if…else… 的处理
- 异常会让函数变得不纯,Either 函子可以用来做异常处理
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this
}
}
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Right.of(fn(this._value))
}
}
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({ error: e.message })
}
}
parseJSON('{ name: xxx }') // Left { _value: {error: 'Unexpected token n in JSON at position 2'} }
IO
- IO 函子中 _value 是一个函数,这里是把函数作为值来处理
- IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),让当前的操作变为纯的操作
- 把不纯的操作交给调用者来处理
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO(function() {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
let r = IO.of(process).map(p => p.execPath) // IO { _value: [Function] }
r._value() // C:\xxx\xxx\xxx\xxx
Task
异步执行
- 异步任务的实现过于复杂,我们使用
folktale
中的Task
来演示 folktale
一个标准的函数式编程库- 和
lodash
、ramda
不同的是,没有提供很多功能函数 - 只提供了一些函数式处理的操作,例如:compose、curry等,一些函子 Task、Either、MayBe等
- 和
const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
let f = curry(2, (x, y) => {
return x + y
})
f(1, 2) // 3
f(1)(2) // 3
let fn = compose(toUpper, first)
fn(['one', 'two']) // ONE
Task处理异步任务
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
function readFile (filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('xxx.xx') // 查找某一文件
.map(fn) // 可以对文件进行某些处理
.map(fn) // 可以对文件进行某些处理(可以进行多次处理)
.run() // 执行
.listen({ // 监听
onRejected: err => console.log(err), // 失败
onResolved: value => console.log(value) // 成功
})
Pointed函子
- Pointed 函子是实现 of 静态方法的函子
- of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map 来处理值)
class Container {
static of (value) {
return new Container(value)
}
...
}
Container.of(6).map(v => v * v)
Monad
- Monad 函子是可以变扁的 Pointed 函子, IO(IO(x)),解决嵌套的问题
- 一个函子如果具有 join 和 of 两个方法,并遵守一些定律就是一个 Monad
const fs = require('fs')
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO(function() {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
flatMap (fn) {
return this.map(fn).join()
}
}
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function (x) {
return new IO(function() {
console.log(x)
return x
})
}
readFile('xxxx.xx').faltMap(print).join()