recompose 库中的 Compose 方法

Compose method in recompose Library

我正在查看@acdlite 编写的重组库中的 compose 函数来为高阶组件组合边界条件,这就是组合函数的样子

const compose = (...funcs) => funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);

不过,我试过Eric-Elliott's one liner approach to compose, from https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d,特别是这段代码。

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

我试过在我的 React 组件中使用这两种变体,

const ListWithConditionalRendering = compose(
  withLoadingIndicator,
  withDataNull,
  withListEmpty
)(Users);

而且它们似乎都工作正常。我无法理解上述功能的工作方式是否有任何差异,如果有,它们是什么。

对于非常小众的场景存在一些差异,了解这些差异可能会有所帮助。

第一个预组合一个函数,这意味着它在组合时调用 reduce() 而不是在调用时调用。相反,第二种方法 returns 作用域函数在 调用 时调用 reduceRight(),而不是在组合时调用。

第一种方法接受数组中最后一个函数的多个参数,而第二种方法只接受一个参数:

const compose1 = (...funcs) => funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
const compose2 = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

const f = s => (...args) => (console.log('function', s, 'length', args.length), args);

compose1(f(1), f(2), f(3))(1, 2, 3);
compose2(f(4), f(5), f(6))(1, 2, 3);

如果函数数组非常大,第一种方法可能会导致堆栈溢出,因为它是预先组合的,而第二种方法(相对)堆栈安全:

const compose1 = (...funcs) => funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
const compose2 = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

const f = v => v;

try {
  compose1.apply(null, Array.from({ length: 1e5 }, () => f))();
  console.log('1 is safe');
} catch (e) {
  console.log('1 failed');
}

try {
  compose2.apply(null, Array.from({ length: 1e5 }, () => f))();
  console.log('2 is safe');
} catch (e) {
  console.log('2 failed');
}

† 如果...fns太大,第二种方法仍然会导致堆栈溢出,因为arguments也分配在堆栈上。

如果您对 reduce-composition 实际构建的结构感兴趣,可以将其可视化如下:

/* original:
const compose = (...funcs) =>
funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
*/

const compose = (...funcs) =>
  funcs.reduce((a, b) => `((...args) => ${a}(${b}(...args)))`, $_("id"));

const $_ = name =>
  `${name}`;

const id = x => x;
const inc = x => x + 1;
const sqr = x => x * x;
const neg = x => -x;

const computation = compose($_("inc"), $_("sqr"), $_("neg"));

console.log(computation);

/* yields:
((...args) => ((...args) => ((...args) =>
  id(inc(...args))) (sqr(...args))) (neg(...args)))
*/

console.log(eval(computation) (2)); // 5 (= id(inc(sqr(neg(2))))

那么这是怎么回事?我用模板字符串替换了内部函数 (...args) => a(b(...args)),用 $_ 辅助函数替换了 arg => arg。然后我将 Template-String 括在括号中,这样得到的 String 代表一个 IIFE。最后但并非最不重要的是,我将 $_ 具有专有名称的辅助函数传递给 compose.

$_ 有点奇怪,但它确实有助于可视化 unapplied/partially 应用函数。

从计算结构可以看出,reduce-composition构建了匿名函数的嵌套结构,rest/spread操作分散在整个代码中。

可视化和解释部分应用的函数很困难。我们可以通过省略内部匿名函数来简化它:

const compose = (...funcs) =>
  funcs.reduce($xy("reducer"), $_("id"));

const $_ = name =>
  `${name}`;

const $xy = name => (x, y) =>
  `${name}(${x}, ${y})`;

const id = x => x;
const inc = x => x + 1;
const sqr = x => x * x;
const neg = x => -x;

console.log(
  compose($_("inc"), $_("sqr"), $_("neg"))
  // reducer(reducer(reducer(id, inc), sqr), neg)
);

我们实际上可以进一步简化 运行 组成:

const compose = (...funcs) =>
  funcs.reduce((a, b) => (...args) => a(b(...args)), $x("id"));

const $x = name => x =>
  `${name}(${x})`;

console.log(
  compose($x("inc"), $x("sqr"), $x("neg")) (2) // id(inc(sqr(neg(2))))
);

我相信像这样的复杂计算的可视化是一种强大的技术,可以正确理解它们并更好地理解 nested/recursive 计算结构。

实施展示和讲述?好的-

const identity = x =>
  x

const compose = (f = identity, ...fs) => x =>
  f === identity
    ? x
    : compose (...fs) (f (x))
    
const add1 = x =>
  x + 1
  
console .log
  ( compose () (0)                   // 0
  , compose (add1) (0)               // 1
  , compose (add1, add1) (0)         // 2
  , compose (add1, add1, add1) (0)   // 3
  )

或者不使用 compose 内联 ...

const ListWithConditionalRendering = compose(
  withLoadingIndicator,
  withDataNull,
  withListEmpty
)(Users);

你可以创建一种 "forward composition" 参数在前的函数 -

const $ = x => k =>
  $ (k (x))
  
const add1 = x =>
  x + 1
  
const double = x =>
  x * 2

$ (0) (add1) (console.log)
// 1

$ (2) (double) (double) (double) (console.log)
// 16

$ (2) (double) (add1) (double) (console.log)
// 10

$ 在您可以保持 -

模式时很有用
$ (value) (pureFunc) (pureFunc) (pureFunc) (...) (effect)

上面,$ 将一个值放入某种 "pipeline" 中,但是没有办法将值 取出 。一个小的调整允许我们编写非常灵活的可变参数表达式。下面,我们使用 $ 作为分隔管道表达式开始和结束的方式。

const $ = x => k =>
  k === $
    ? x
    : $ (k (x))

const double = x =>
  x * 2

const a =
  $ (2) (double) ($)
  
const b =
  $ (3) (double) (double) (double) ($)

console .log (a, b)
// 4 24

这个可变参数接口使您能够编写类似于其他更面向函数的语言中令人垂涎的 |> 运算符的表达式 -

value
  |> pureFunc
  |> pureFunc
  |> ...
  |> pureFunc

5 |> add1
  |> double
  |> double
  // 24

使用 $,转换为 -

$ (value) (pureFunc) (pureFunc) (...) (pureFunc) ($)

$ (5) (add1) (double) (double) ($) // 24

该技术还可以与柯里化函数很好地结合 -

const $ = x => k =>
  $ (k (x))

const add = x => y =>
  x + y
  
const mult = x => y =>
  x * y
  
$ (1) (add (2)) (mult (3)) (console.log)
// 9

或者举个更有趣的例子-

const $ = x => k =>
  $ (k (x))

const flatMap = f => xs =>
  xs .flatMap (f)
  
const join = y => xs =>
  xs .join (y)
  
const twice = x =>
  [ x, x ]

$ ('mississippi')
  (([...chars]) => chars)
  (flatMap (twice))
  (join (''))
  (console.log)
  // 'mmiissssiissssiippppii'