如何使用 Lodash flow 理解 curry 和函数组合? [英] How to understand curry and function composition using Lodash flow?

查看:15
本文介绍了如何使用 Lodash flow 理解 curry 和函数组合?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

import {flow, curry} from 'lodash';const add = (a, b) =>a + b;const square = n =>n * n;const tap = curry((拦截器,n) => {拦截器(n);返回 n;});const trace2 = curry((message, n) => {return tap((n) => console.log(`${message} is ${n}`), n);});const 跟踪 = 标签 =>{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 调用中.任何解释将不胜感激:)

解决方案

Silver Spoon 评估

我们将从跟踪对

的评估开始

addSquare(3, 1)//...

好的,开始

= flow([add, trace('after add'), square]) (3, 1)添加(3,1)4跟踪('添加后')(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);返回 4;console.log(`== ${ '添加后' }: ${ 4 }`);返回 4;~log 效果~ "== 添加后:4";返回 44正方形(4)4*416= 16

因此,您遇到的基本技巧"是 trace('after add') 返回一个等待最后一个参数的函数.这是因为 trace 是一个 柯里化的 2 参数函数.

<小时>

无用

我无法表达对

组合和柯里化配合得很好,因为

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

<小时>

现在让我们重写您的代码

const add = a =>b =>a + bconst 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 * yconst square = n =>n * n;const tap = f =>x =>(f(x), x)const 跟踪 = str =>点击 (x => console.log(`== ${str}: ${x}`))const flow = ([f,...fs]) =>x =>f === 未定义?x : 流量 (fs) (f(x))consttripleSquare = flow([mult(3), trace('triple'), square])控制台.log(tripleSquare(2))//==三重:6"//=>36

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

<小时>

又被骗了

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

consttripleSquare = flow([mult(3), trace('triple'), square])console.log(tripleSquare(2))//=> 36

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

假设我们必须让原始 addSquare 函数可以使用这两个参数 …那会是什么样子?

const add = x =>y =>x + yconst square = n =>n * n;const tap = f =>x =>(f(x), x)const 跟踪 = str =>点击 (x => console.log(`== ${str}: ${x}`))const flow = ([f,...fs]) =>x =>f === 未定义?x : 流量 (fs) (f(x))const addSquare = (x,y) =>flow([add(x), trace('add'), square]) (y)console.log(addSquare(3,1))//==添加:4"//=>16

好的,所以我们必须将 addSquare 定义为这个

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

它当然不如 lodash 版本聪明,但它明确的是如何组合术语,并且几乎复杂性.

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

<小时>

大惊小怪,以及为什么

你的程序中的一切都是一种权衡.我讨厌看到初学者在应该很简单的事情上挣扎.使用使这些事情变得如此复杂的库是非常令人沮丧的 - 甚至不让我开始使用 Lodash 的

  • 从左到右的作曲家flow,这个名字很贴切,因为你的眼睛会流动像你的意大利面一样尝试在数据通过您的程序时对其进行跟踪.(笑)

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

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));

I started by writing trace2 thinking that trace wouldn't work because "How can tap possibly know about n or x whatever?".

But trace does work and I do not understand how it can "inject" the x coming from the flow into the tap call. Any explanation will be greatly appreciated :)

解决方案

Silver Spoon Evaluation

We'll just start with tracing the evaluation of

addSquare(3, 1) // ...

Ok, here goes

= 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

So the basic "trick" you're having trouble seeing is that trace('after add') returns a function that's waiting for the last argument. This is because trace is a 2-parameter function that was curried.


Futility

I can't express how useless and misunderstood the flow function is

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
  }
}

Sure, it "works" as it's described to work, but it allows you to create awful, fragile code.

  • loop thru all provided functions to type check them
  • loop thru all provided functions again to apply them
  • for some reason, allow for the first function (and only the first function) to have special behaviour of accepting 1 or more arguments passed in
  • all non-first functions will only accept 1 argument at most
  • in the event an empty flow is used, all but your first input argument is discarded

Pretty weird f' contract, if you ask me. You should be asking:

  • why are we looping thru twice ?
  • why does the first function get special exceptions ?
  • what do I gain with at the cost of this complexity ?

Classic Function Composition

Composition of two functions, f and g – allows data to seemingly teleport from state A directly to state C. Of course state B still happens behind the scenes, but the fact that we can remove this from our cognitive load is a tremendous gift.

Composition and currying play so well together because

  1. function composition works best with unary (single-argument) functions
  2. curried functions accept 1 argument per application


Let's rewrite your code now

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

"Hey you tricked me! That comp2 wasn't easy to follow at all!" – and I'm sorry. But it's because the function was doomed from the start. Why tho?

Because composition works best with unary functions! We tried composing a binary function add with a unary function square.

To better illustrate classical composition and how simple it can be, let's look at a sequence using just unary functions.

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

Oh, by the way, we reimplemented flow with a single line of code, too.


Tricked again

OK, so you probably noticed that the 3 and 2 arguments were passed in separate places. You'll think you've been cheated again.

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

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

But the fact of the matter is this: As soon as you introduce a single non-unary function into your function composition, you might as well refactor your code. Readability plummets immediately. There's absolutely no point in trying to keep the code point-free if it's going to hurt readability.

Let's say we had to keep both arguments available to your original addSquare function … what would that look like ?

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

OK, so we had to define addSquare as this

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

It's certainly not as clever as the lodash version, but it's explicit in how the terms are combined and there is virtually zero complexity.

In fact, the 7 lines of code here implements your entire program in less than it takes to implement just the lodash flow function alone.


The fuss, and why

Everything in your program is a trade off. I hate to see beginners struggle with things that should be simple. Working with libraries that make these things so complex is extremely disheartening – and don't even get me started on Lodash's curry implementation (including it's insanely complex createWrap)

My 2 cents: if you're just starting out with this stuff, libraries are a sledgehammer. They have their reasons for every choice they made, but know that each one involved a trade off. All of that complexity is not totally unwarranted, but it's not something you need to be concerned with as a beginner. Cut your teeth on basic functions and work your way up from there.


Curry

Since I mentioned curry, here's 3 lines of code that replace pretty much any practical use of Lodash's curry.

If you later trade these for a more complex implementation of curry, make sure you know what you're getting out of the deal – otherwise you just take on more overhead with little-to-no gain.

// 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)


Two types of function composition

One more thing I should mention: classic function composition applies functions right-to-left. Because some people find that hard to read/reason about, left-to-right function composers like flow and pipe have showed up in popular libs

  • Left-to-right composer, flow, is aptly named because your eyes will flow in a spaghetti shape as your try to trace the data as it moves thru your program. (LOL)

  • Right-to-left composer, composer, will make you feel like you're reading backwards at first, but after a little practice, it begins to feel very natural. It does not suffer from spaghetti shape data tracing.

这篇关于如何使用 Lodash flow 理解 curry 和函数组合?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆