深入函数式编程(FP)
FP的要求
在函数式编程语言世界里面:
function必须是一等公民。一等公民是指function可以作为一个function参数,也可以作为function返回值,也可以赋值给变量或者其他对象属性。
JavaScript中,function就是一等公民。
引用透明。同样的in params,不论多少次,返回必须要一样,没有造成副作用(side effect),函数是纯函数(pure function)。
什么是Side Effect?修改传入的参数、外部状态、发送http请求、db查询、打印log、获取input、dom查询、访问(系统)状态等等
将简单的指令式调用封装为函数。例如👇
1
2
3
4
5
6
7
8
9
function formatter(formatFn) {
return function inner(str){
return formatFn( str );
};
}
var lower = formatter( function (v){
return v.toLowerCase();
} );
FP的优势
- 低复杂度:function不会有状态,也不会直接存取或读取外接状态。对于相同输入,一定会有相同输出。
- 无需语句(statement):所有Pure Functional Programming Languages 都是由表达式(expression) 所组成的,这跟其他大多数语言不同,大多数程式语言由表达式(expression) 和语句(statement) 组成。
Pure Function
纯函数是指没有副作用的函数。在js中,最常见的是如果将对象传给参数,那么函数拿到的是对象的引用,内部就可能会修改外部数据状态,函数不再pure了。
可以使用不可变数据来避免这种情况,可以参考 ImmutableJS 实战 、ES6语法中的let/const与Object.freeze()/Object.isFrozen()
Higher-order Function
高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
Closure
闭包定义
当一个函数可以记住并存取到不同scope(作用域)的变量,甚至这个函数在不同scope被执行,称之Closure
举个例子:
1
2
3
4
5
6
7
function greaterThan(n) {
return function inner (m) { return m > n; };
}
var greaterThan10 = greaterThan(10);
console.log(greaterThan10(11)); // true
n的scopre是greaterThan。greaterThan运行之后,按理说n应该被回收。但是由于inner中用到了n,所以它被“暂时保留”了下来。这个称之为:n被内部函数inner closure。
Partial Application
偏函数应用定义:partial application是指一种减少函数参数个数Arity的过程。
什么
Arity?
指的是形式参数parameter的个数。js中可以通过func.length获取到必填参数的个数。
偏函数工具函数实现:
1
2
3
4
5
function partial(fn, ...presetArgs) {
return function partiallyApplied(...laterArgs) {
return fn( ...presetArgs, ...laterArgs )
}
}
可以利用偏函数,基于基础函数(参数多,更灵活),二次封装函数(参数少,针对某种场景)。例如:
1
2
3
4
5
6
function ajax( url, data, cb ) { } // ajax 异步请求
let getOrder = partial(ajax, "http://some.api/order") // 用于请求order接口的异步函数
let getLastOrder = partial(getOrder, { id: ORDER_ID }) // 用于获取最后一个order的异步函数
let getLastOrder2 = partial(ajax, "http://some.api/order", { id: ORDER_ID }) // 第二种定义方法
偏函数用途:
- 切割传参数的时空背景(时间、程式的不同区块),原本的方式需要在调用的时候立刻传入所有的参数,如果你的函数中有些参数待会才传入,可以考虑使用currying和partial application。
- 实现柯里化(见下一部分)
- 可以隐藏细节,增强可读性(例如上面基于ajax的封装)
参考文章:
Point-free style
Point-free(又写成Pointfree,中文:无参数,无点),正式名称为:tacit programming,其中的point(点)指的就是函数的parameter(形式参数)。
作用:Pointfree透过隐藏parameter - argument(形参-实参对应),减少视觉上的干扰,上层操作不直接操作数据,只合成运算过程。
举个例子:
1
2
3
4
5
function double(x) {
return x * 2
}
[1,2,3,4,5].map( double ) // point free style
[1,2,3,4,5].map( v => double(v) ) // not point free style
Functor 函子
定义
A functor is something that can be mapped over.
来自 Haskell 和 Fantasy Land specification。
那something 是什么?就是一组值放在某个容器(集合)里,容器就是指这些值怎么摆放,比如说阵列(依序列)或者物件(用key 来取值…等)。
那什么叫that can be mapped over?也就是js中map(..) 做的事,把每个值经过mapper(..) 得到新值,最后再把新值依照放进同样结构的容器后return 。
JS的Array对象上,使用Map还是forEach呢?
map(..)是用来映射值的,不是来产生副作用的。如果要传入带有副作用的函数,建议还是使用forEach(..)或者干脆写 loop 避免造成困惑。
总结来说,任何具有map方法(映射关系)的资料结构,都可以视为functor。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Wrapper {
constructor (value) {
this.value = value
}
map (f) {
return new Wrapper(f(this.value))
}
}
// 使用代码
let something = new Wrapper(2) // something => { value: 2 }
let otherthing = something.map(function (value) {
return value + 3;
}) // otherthing => { value: 5 }
class Wrapper 就可以视作一个 functor,因为它具有map方法。而且在map方法中,通过传入的mapper function,使内部值转化,最后返回映射后的新值。
Continuation-passing style(CPS)
在JS中,continuation表示函数结束后下一步骤的callback,也就是接着要做的事,而CPS就是在函数结束后把要做的事指定给下一个函数(当作参数)。
我理解是这种回调callback风格就是cps。
代码参考:
1
2
3
4
5
6
7
8
9
10
11
12
13
"use strict";
var sumRecursion = (function IIFE(){
return function sum(...nums) {
return recursion(nums, v=>v)
}
function recursion([result, ...nums], cont) {
if (nums.length == 0) return cont(result)
return recursion( nums, function(v) {
return cont(result + v)
})
}
})();
console.log(sumRecursion( 3, 1, 17, 94, 8 ));
可以参考:
参数处理
过滤参数
场合:将两个函数组合,比如说把A function 传入B function ,但此时B function 传入的参数跟A function 数量不符合。比如说:
- B:map(…)传3个变数
value,index,array - A:parseInt(…)接收两个变数
string,radix
例如:
1
2
3
['1', '2'].map(parseInt)
// 返回:[1, NaN]
// 因为'2'传进来相当于执行:parsetInt('2', 1)。1进制肯定是不可能出现'2'
封装unary函数,仅让一个参数能通过:
1
2
3
4
5
function unary(fn) {
return function onlyOneArg(arg){
return fn( arg );
};
}
使用效果:
1
2
['1', '2'].map(unary(parseInt)) // parseint经过包装后,只会接收到第1个参数
// 返回:[1, 2]
传1返1
基础函数:接受一个参数,然后原封不动返回
1
2
3
function identical(v) {
return v
}
这样某些场景下,就不用写() =>v 的语法,例如:
1
2
str.split().filter(identical) // 过滤空串
promise1.then(identical(val)).then(func1)
条件判断
取反函数:
1
2
3
4
5
function not (testerfn) {
return function negated (...args) {
return !testerfn(...args)
}
}
条件函数:
1
2
3
4
5
6
7
function when (testerfn, fn) {
return function conditional (...args) {
if (testerfn(...args)) {
return fn(...args)
}
}
}