【问题标题】:How to understand curry and function composition using Lodash flow?如何使用 Lodash 流理解 curry 和函数组合?
【发布时间】:2017-06-27 02:40:23
【问题描述】:
import {flow, curry} from 'lodash';

const add = (a, b) => a + b;

const square = n => n * n;

const tap = curry((interceptor, n) => {
    interceptor(n);
    return n;
});

const trace2 = curry((message, n) => {
    return tap((n) => console.log(`${message} is  ${n}`), n);
});

const trace = label => {
    return tap(x => console.log(`== ${ label }:  ${ x }`));
};


const addSquare = flow([add, trace('after add'), square]);
console.log(addSquare(3, 1));

我开始编写 trace2 时认为 trace 不起作用,因为“tap 怎么可能知道 n 或 x 什么的?”。

但是 trace 确实有效,我不明白它如何将来自流的 x “注入”到 tap 调用中。任何解释将不胜感激:)

【问题讨论】:

  • tap 是柯里化的,这意味着如果您没有传递足够多的参数,它会返回一个仍然需要其余参数的函数。

标签: functional-programming lodash


【解决方案1】:

银勺评估

我们将从跟踪评估开始

addSquare(3, 1) // ...

好的,这就去

= flow([add, trace('after add'), square]) (3, 1)
        add(3,1)
        4
             trace('after add') (4)
             tap(x => console.log(`== ${ 'after add' }:  ${ x }`)) (4)
             curry((interceptor, n) => { interceptor(n); return n; }) (x => console.log(`== ${ 'after add' }:  ${ x }`)) (4)
             (x => console.log(`== ${ 'after add' }:  ${ x }`)) (4); return 4;
             console.log(`== ${ 'after add' }:  ${ 4 }`); return 4;
~log effect~ "== after add: 4"; return 4
             4
                                 square(4)
                                 4 * 4
                                 16
= 16

因此,您无法看到的基本“技巧”是trace('after add') 返回一个正在等待最后一个参数的函数。这是因为trace 是一个 curried 的 2 参数函数。


无用

我无法表达flow函数是多么无用和误解

function flow(funcs) {
  const length = funcs ? funcs.length : 0
  let index = length
  while (index--) {
    if (typeof funcs[index] != 'function') {
      throw new TypeError('Expected a function')
    }
  }
  return function(...args) {
    let index = 0
    let result = length ? funcs[index].apply(this, args) : args[0]
    while (++index < length) {
      result = funcs[index].call(this, result)
    }
    return result
  }
}

当然,它“工作”,因为它被描述为工作,但它允许您创建糟糕、脆弱的代码。

  • 循环遍历所有提供的函数以对它们进行类型检查
  • 再次循环所有提供的函数以应用它们
  • 出于某种原因,允许 first 函数(并且第一个函数)具有接受 1 个或更多参数的特殊行为传入
  • 所有非第一个函数最多只接受一个参数
  • 如果使用空流,则除第一个输入参数之外的所有参数都将被丢弃

如果你问我,这个合同很奇怪。你应该问:

  • 为什么我们要循环两次?
  • 为什么第一个函数会出现特殊异常?
  • 以这种复杂性为代价,我能得到什么?

经典函数组合

fg 这两个函数的组合 - 允许数据看似从状态 A 直接传送到状态 C。当然状态B 仍然发生在幕后,但我们可以将其从认知负荷中移除这一事实是一个巨大的礼物。

构图和咖喱配合得很好,因为

  1. 函数组合最适合一元(单参数)函数
  2. curried 函数每个应用程序接受 1 个参数

让我们现在重写你的代码

const add = a => b => a + b

const square = n => n * n;

const comp = f => g => x => f(g(x))

const comp2 = comp (comp) (comp)

const addSquare = comp2 (square) (add)

console.log(addSquare(3)(1)) // 16

“嘿,你骗了我!comp2 一点都不容易跟上!”——我很抱歉。但这是因为这个功能从一开始就注定了。为什么?

因为合成最适合一元函数!我们尝试将二元函数 add 与一元函数 square 组合在一起。

为了更好地说明经典组合以及它的简单程度,让我们看一下使用只是一元函数的序列。

const mult = x => y => x * y

const square = n => n * n;

const tap = f => x => (f(x), x)

const trace = str => tap (x => console.log(`== ${str}: ${x}`))

const flow = ([f,...fs]) => x =>
  f === undefined ? x : flow (fs) (f(x))

const tripleSquare = flow([mult(3), trace('triple'), square])

console.log(tripleSquare(2))
// == "triple: 6"
// => 36

哦,顺便说一句,我们也用一行代码重新实现了flow


又被骗了

好的,所以您可能注意到 32 参数是在不同的地方传递的。你会认为你又被骗了。

const tripleSquare = flow([mult(3), trace('triple'), square])

console.log(tripleSquare(2)) //=> 36

但事实是这样的:一旦你在你的函数组合中引入了一个非一元函数,你还不如重构你的代码。可读性立即下降。如果会损害可读性,那么尝试保持代码无点是绝对没有意义的。

假设我们必须保留原始 addSquare 函数的两个参数......那会是什么样子?

const add = x => y => x + y

const square = n => n * n;

const tap = f => x => (f(x), x)

const trace = str => tap (x => console.log(`== ${str}: ${x}`))

const flow = ([f,...fs]) => x =>
  f === undefined ? x : flow (fs) (f(x))

const addSquare = (x,y) => flow([add(x), trace('add'), square]) (y)

console.log(addSquare(3,1))
// == "add: 4"
// => 16

好的,所以我们必须这样定义addSquare

const addSquare = <b>(x,y) =&gt;</b> flow([add<b>(x)</b>, trace('add'), square]) <b>(y)</b>

它当然不像 lodash 版本那样聪明,但它明确在术语的组合方式上并且几乎复杂性。

事实上,这里的 7 行代码实现整个程序所需的时间比单独实现 lodash flow 函数要少。


大惊小怪,以及为什么

您的程序中的所有内容都是一种权衡。我讨厌看到初学者在应该简单的事情上挣扎。与使这些事情变得如此复杂的库一起工作是非常令人沮丧的——甚至没有让我开始使用 Lodash 的 curry 实现(包括它疯狂复杂的 createWrap

我的 2 美分:如果您刚开始使用这些东西,图书馆就是一把大锤。他们做出的每一个选择都有自己的理由,但他们知道每一个选择都需要权衡取舍。所有这些复杂性并非完全没有根据,但作为初学者,这不是您需要关心的事情。在基本功能上磨练你的牙齿,然后从那里开始。


咖喱

由于我提到了curry,这里有 3 行代码几乎可以替代 Lodash 咖喱的任何实际用途。

如果您以后用这些来换取更复杂的 curry 实现,请确保您知道从交易中得到什么 - 否则您只会承担更多开销而几乎没有收益。

// for binary (2-arity) functions
const curry2 = f => x => y => f(x,y)

// for ternary (3-arity) functions
const curry3 = f => x => y => z => f(x,y,z)

// for arbitrary arity
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)

两种类型的函数组合

我还要提一提:经典函数组合从右到左应用函数。因为有些人觉得很难阅读/推理,所以像 flowpipe 这样的从左到右的函数作曲家已经出现在流行的库中

  • 从左到右的作曲家flow,这个名字恰如其分,因为当你试图追踪数据时,你的眼睛会流动成意大利面条形状当它通过您的程序时。 (笑)

  • 从右到左作曲家,composer,一开始会让你觉得你是在倒着读,但经过一点练习,它开始感觉很自然。它不受意大利面条形状数据跟踪的影响。

【讨论】:

  • @Tirke,您的问题比您可能意识到的要广泛,我只对此处介绍的主题进行了初步探讨。如果我能以任何其他方式提供帮助,请告诉我^_^
  • 我做梦都想不到这么详细和深思熟虑的答案。这正是我所需要的,我现在肯定有更深入的了解。目前我对流程或构图没有任何疑问,但如果我再次陷入 fp 的精彩世界,我会记住你的 :) 谢谢 1000x。
  • @Tirke 乐于助人!我又添加了一条关于 RTL 与 LTR 函数组合的注释。毫不奇怪,flow 的存在让你的生活变得更加艰难——一旦你了解了compose,你会想知道为什么有人试图“修复”尚未破坏的东西。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-06
  • 1970-01-01
  • 1970-01-01
  • 2018-11-03
  • 2015-06-19
  • 1970-01-01
相关资源
最近更新 更多