4.模块化且可重用的代码

函数链与函数管道的比较
Ramda.js 函数库
柯里化、部分应用(partial application)和函数绑定
通过函数式组合构建模块化程序
利用函数组合子增强程序的控制流

先将问题分解成较小的部分,再重建这些部分以形成整体的解决方案。

方法链与函数管道的比较

Haskell中定义函数的符号。该符号先给出了函数的名称,随后用一个操作符来设置函数的输入和输出类型

方法链接(紧耦合,有限的表现力)

它与方法所属的对象紧紧地耦合在一起,限制链中可以使用的方法数量,也就限制了代码的表现力。这样就只能够使用由Lodash提供的操作,而无法轻松地将不同函数库的(或自定义的)函数连接在一起。

1
2
3
4
5
6
7
_.chain(names)
.filter(isValid) //每一个“点”后只能调用 Lodash 提供的方法
.map(s => s.replace(/_/, ' '))
.uniq()
.map(_.startCase)
.sort()
.value();

函数的管道化(松耦合,灵活)

方法链接通过对象的方法紧密连接;而管道以函数作为组件,将函数的输入和输出松散地连接在一起。但是,为了实现管道,被连接的函数必须在元数(arity)和类型上相互兼容。

函数式编程将管道视为构建程序的唯一方法.

对于不同的任务,问题的定义与解决方案间总是存在很大的差异。因此,特定的计算必须在特定的阶段进行。这些阶段由不同的函数表征,而所选函数的输入和输出需要满足以下两个兼容条件。

类型=>函数的返回类型必须与接收函数的参数类型相匹配。

元数=>接收函数必须声明至少一个参数才能处理上一个函数的返回值。

元组

对传入的参数进行参数类型,和个数的校验,可以用TS替代

  • 不可变的——一旦创建,就无法改变一个元组的内部内容。
  • 避免创建临时类型——元组可以将可能毫不相关的数据相关联。而定义和实例化一些仅用于数据分组的新类型使得模型复杂并令人费解。
  • 避免创建异构数组——包含不同类型元素的数组使用起来颇为困难,因为会导致代码中充满大量的防御性类型检查。传统上,数组意在存储相同类型的对象。

柯里化

要求所有参数都被明确地定义,因此当使用部分参数调用时,它会返回一个新的函数,在真正运行之前等待外部提供其余的参数。

R._curry1

1
2
3
4
5
6
7
8
9
10
11
function curry1(fn) {
return function f1(a) {
// 判断a是否为占位符
if (arguments.length === 0 || _isPlaceholder(a)) {
return f1
} else {
// 多余的参数也可以传入
return fn.apply(this, arguments);
}
}
}

部分应用和函数绑定

  • 柯里化在每次分步调用时都会生成嵌套的一元函数。在底层,函数的最终结果是由这些一元函数的逐步组合产生的。同时,curry的变体允许同时传递一部分参数。因此,可以完全控制函数求值的时间与方式。

  • 部分应用将函数的参数与一些预设值绑定(赋值),从而产生一个拥有更少参数的新函数。该函数的闭包中包含了这些已赋值的参数,在之后的调用中被完全求值。

延迟函数绑定

当期望目标函数使用某个所属对象来执行时,使用函数绑定来设置上下文对象就变得尤为重要。例如,浏览器中的setTimeout和setInterval等函数,如果不将this的引用设为全局上下文,即window对象,是不能正常工作的。传递undefined在运行时正确设置它们的上下文。

1
2
3
4
5
6
7
8
9
10
11
const Scheduler = (function () {
const delayedFn = _.bind(setTimeout, undefined, _, _);
return {
delay5: _.partial(delayedFn, _, 5000),
delay10: _.partial(delayedFn, _, 10000),
delay: _.partial(delayedFn, _, _)
};
})();
Scheduler.delay5(function () {
consoleLog('Executing After 5 seconds!')
});

函数组合

1
2
Function.prototype.compose = R.compose;
const cleanInput = checkLengthSsn.compose(normalize).compose(trim);

对副作用的处理是,拆分副作用的函数,并且固定副作用函数的参数

point-free编程

使用compose(或者pipe)就意味着永远不必再声明参数了(称为函数的points),这无疑会使代码更加声明式、更加简洁,或更加point-free。

1
R.compose(first, getName, reverse, sortByGrade, combine);

point-free编程使JavaScript的函数式代码更接近于Haskell和UNIX的理念。它可以用来提高抽象度,促使开发者关注高级组件的组合,而不是低级的函数求值的细节。柯里化在这里也起着很重要的作用,因为它能够灵活地部分定义一个只差最后一个参数的内联函数。这种编码风格也被称为Tacit编程。

在将组合改为这种编码风格时,要记住,过度的使用会使得程序晦涩且令人费解。

组合子

组合器是一些可以组合其他函数(或其他组合子),并作为控制逻辑运行的高阶函数。组合子通常不声明任何变量,也不包含任何业务逻辑,它们旨在管理函数式程序的流程。除了compose和pipe,还有无数的组合子,一些最常见的组合子如下。

  • identity(I-combinator)

为以函数为参数的更高阶函数提供数据,如之前清单4.12中的point-free代码。
在单元测试的函数组合器控制流中作为简单的函数结果来进行断言。例如,可以使用identity函数来编写compose的单元测试。
函数式地从封装类型中提取数据

1
//identity :: (a) -> a
  • tap(K-组合子)

该函数接收一个输入对象a和一个对a执行指定操作的函数。它使用提供的对象调用给定的函数,然后再返回该对象

1
2
3
4
function tap(fn, x) {
fn(x);
return x;
}
  • alt (OR-组合子) alternation

alt组合子能够在提供函数响应的默认行为时执行简单的条件逻辑。该组合器以两个函数为参数,如
果第一个函数返回值已定义(即,不是false、null或undefined)

用于处理if else 逻辑

1
2
3
4
5
const alt = function (func1, func2) {
return function (val) {
return func1(val) || func2(val);
}
};
  • seq(S-组合子) sequence

seq组合子用于遍历函数序列。它以两个或更多的函数作为参数并返回一个新的函数,会用相同的值顺序调用所有这些函数

seq组合子不会返回任何值,只会一个一个地执行一系列操作。如果要将其嵌入函数组合之间,可以使用R.tap将它与其余部分进行桥接。

1
2
3
4
5
6
7
8
const seq = function(/*funcs*/) {
const funcs = Array.prototype.slice.call(arguments);
return function (val) {
funcs.forEach(function (fn) {
fn(val);
});
};
};
  • fork(join)组合子

1
2
3
4
5
const fork = function(join, func1, func2){
return function(val) {
return join(func1(val), func2(val));
};
};
1
2
3
const eqMedianAverage = fork(R.equals, R.median, R.mean);
eqMedianAverage([80, 90, 100])); //-> True
eqMedianAverage([81, 90, 100])); //-> False

总结

  • 用于连接可重用的、模块化的、组件化程序的函数链与管道。
  • Ramda.js是一个功能强大的函数库,适用于函数的柯里化与组合。
  • 可以通过部分求值和柯里化来减少函数元数,利用对参数子集的部分求值将函数转化为一元函数。
  • 可以将任务分解为多个简单的函数,再通过组合来获得整个解决方案。
  • 以point-free的风格编写,并用函数组合子来组织的程序控制流,可解决现实问题。
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信