如何使用Lodash流理解咖喱和功能组成? [英] How to understand curry and function composition using Lodash flow?
问题描述
从'lodash'导入{flow,curry};
const add =(a,b)=> a + b;
const square = n => n * n; $(b)
$ b const tap = curry((interceptor,n)=> {
interceptor(n);
return n;
}); $($)$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $' ),n);
});
const trace = label => {
return tap(x => console.log(`== $ {label}:$ {x}`));
};
const addSquare = flow([add,trace('add'后),square]);
console.log(addSquare(3,1));
我开始写trace2认为跟踪不起作用,因为如何才能点击可能知道n或者是什么?。
但是跟踪确实有效,我不明白它是如何将来自流的x注入到tap调用中的。任何解释将不胜感激:)
银匙评估
我们将开始追踪
addSquare(3,1)// ...
好的,在这里
<$ (3,1)
add(3,1)
4 $ b $($)$ {code> = flow([add,trace('after add'),square] (4)
tap(x => console.log('== $ {'add'}:$ {x}`))(4)
curry((interceptor,n)=> {interceptor(n); return n;})(x => console.log('== $ {'在add'}之后:$ {x}`))(4 )
(x => console.log('== $ {'在添加'}之后:$ {x}`))(4);返回4;
console.log(`== $ {'在添加'}之后:$ {4}`);返回4;
〜log效果〜==添加后:4;返回4
4
square(4)
4 * 4
16
= 16
所以你看到的基本技巧是 trace('add'后面)
返回一个函数等待最后一个论点。这是因为 trace
是一个2参数函数,它是 curried 。
徒劳无用
我无法表达如何无用和误解实现(包括 复杂
-
从左到右的作曲家,
flow
,被恰当地命名,因为你的眼睛会流动以意大利式的形状呈现,以便在您的程序移动时跟踪数据。 (LOL) 会让你觉得你刚开始倒退,但经过一些练习后,它开始感觉很自然。它不会受到意大利面形状数据追踪的影响。
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
- function composition works best with unary (single-argument) functions
- 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流理解咖喱和功能组成?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!