深入函数式编程(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 }) // 第二种定义方法

偏函数用途:

  • 切割传参数的时空背景(时间、程式的不同区块),原本的方式需要在调用的时候立刻传入所有的参数,如果你的函数中有些参数待会才传入,可以考虑使用curryingpartial application
  • 实现柯里化(见下一部分)
  • 可以隐藏细节,增强可读性(例如上面基于ajax的封装)

参考文章:

bookmark

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.
来自 HaskellFantasy 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 ));

可以参考:

bookmark

参数处理

过滤参数

场合:将两个函数组合,比如说把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)
}
}
}

参考文章

link_preview

bookmark

link_preview