不幸的是,我买的第一本关于 JavaScript 的书并不适合初学者。 这本书是 Luis Atencio 的《JavaScript 的乐趣》。两年后我仍在尝试理解这本书中的一些概念......
有一个代码我今天还是不明白。你能让我明白这怎么可能吗?
const identity = a => a;
const fortyTwo = () => 42;
const notNull = a => a !== null;
const square = a => a ** 2;
const safeOperation = (operation, guard, recover) =>
input => guard(input, operation) || recover();
const onlyIf = validator => (input, operation) =>
validator(input) ? operation(input) : NaN;
const orElse = identity;
const safeSquare = safeOperation(
square,
onlyIf(notNull),
orElse(fortyTwo)
);
console.log( safeSquare(2) ); // 4
console.log( safeSquare(null) ); // 42
当
safeSquare(2)
是一个具有 3 个参数的函数(safeSquare
、square
、onlyIf(null)
)时,这怎么可能做到 orElse(fortyTwo)
。我认为这里的2是safeOperation
中的输入,但是2在safeOperation
函数中是如何传递的。我只是不明白这个。
我知道柯里化的基础知识,但在这里我不明白。
首先,我们来谈谈一些理论。即,什么是β减少。维基百科有很多理论,但简单的解释是你可以用它的应用程序替换函数。例如,如果您有
const fn = x => x + 1;
const result = fn(2);
最后一部分可以替换为函数体:
const fn = x => x + 1;
// ^^^^^^ -----+ for f(2)
// | then x = 2
const result = 2 + 1; // <--+ therefore we can substitute here
这两段代码是等价的。 β 缩减的名称并不重要,重要的是这里的原理。函数的调用可以用函数体替换,但参数被替换。
即使函数返回另一个函数,这也成立:
const add = a => b => a + b;
这是一个带有一个参数的函数,返回第二个带有另一个参数的函数。当调用第二个函数时,那么最终的结果就是第一个参数a
加上第二个参数
b
。为了清楚起见,这与更长的书写方式相同:
function add(a) {
return function(b) {
return a + b;
}
}
但是,如果我们应用 beta 缩减,则应用与以前相同的规则,调用 add(2)
可以替换为正文,其中
a = 2
:
const add = a => b => a + b;
// ^^^^^^^^^^^ -----+ for add(2)
// | a = 2
const result1 = b => 2 + b; // <--+ therefore we can substitute
const result2 = add(2); // same as the above
console.log( result1(3) ); // 2 + 3 = 5
console.log( result2(3) ); // 2 + 3 = 5
现在我们可以看看如何使用它。
现在练习一下
当
safeSquare(2)
是一个具有 3 个参数的函数时,这怎么可能safeSquare
(square
、onlyIf(null)
、orElse(fortyTwo)
)
safeOperation
函数是带有三个参数的函数。因此,它的调用可以用body来代替
const safeOperation = (operation, guard, recover) =>
input => guard(input, operation) || recover();
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the body that we can substitute
制作
const safeSquare = safeOperation(
square, // ------------------------------------------------+
onlyIf(notNull), // ---------------------------------------------+ |
orElse(fortyTwo) // ------------------------------------------+ | |
); // | | |
// equivalent to: | | |
input => onlyIf(notNull)(input, square) || orElse(fortyTwo)()// | | |
// ^^^^^^^^^^^^^^^ ^^^^^^ ^^^^^^^^^^^^^^^^ | | |
// ^ ^ ^ | | |
// | | | | | |
// | | +------------+ | |
// | +--------------------------------+ |
// +-----------------------------------------------------+
我们可以继续测试减少其他功能。然而,这留给读者练习。这里的要点是,当使用三个参数调用 safeOperation
时,它返回一个只有一个参数的函数。该参数函数被分配给
safeSquare
,这就是为什么
safeSquare(2)
是一个合法的调用。回到理论和术语
一流函数(另请参阅维基百科。简单地说,“一流”意味着这是编程语言可以直接处理的一种值类型 - 它可以分配给变量,它可以传递到函数调用等中。例如,其他第一类类型是字符串或数字。我们可以用它们做同样的事情:
const str = "hello";
const num = 4;
someFunction( "a string" );
otherFunction( 42 );
当一等函数成为可能时,我们还会得到以下结果:高阶函数
Array#map()
(以及其他数组方法)。这两个遍历数组并对每个成员执行 something : .filter()
将包含或排除它,而
.map()
将更改它。对于所有对
.filter()
或
.map()
的调用,检查数组的逻辑是相同的,但检查项目的方式由回调确定:
const fruits = [ 1, 2, 3, 4, 5 ];
const onlyOdd = fruits.filter( x => x % 2 === 1 );
console.log(onlyOdd); // [ 1, 3, 5 ]
const allPlus20 = fruits.map( x => x + 20 );
console.log(allPlus20); // [ 21, 22, 23, 24, 25 ]
.as-console-wrapper { max-height: 100% !important; }
函数式编程依赖于高阶函数来构建应用程序并处理数据。
柯里化
const regularAdd = (a, b) => a + b;
const curriedAdd = a => b => a + b;
const regularPythagoreanCheck = (a, b, c) => a**2 + b**2 === c**2;
const curriedPythagoreanCheck = a => b => c => a**2 + b**2 === c**2;
//distance between two 2D points
const regularDistance = (x1, y1, x2, y2) => Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 );
const curriedDistance = x1 => y1 => x2 => y2 => Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 );
等等。当主要在函数中工作时,这种类型的转换非常有用。但我不会太深入——值得记住的是,没有任何高阶函数被柯里化。只要它是一系列单参数函数即可。现代柯里化的实现没有这那么严格。一种现代的柯里化方法将在每一步接受一个或多个参数,直到满足所有预期参数。例如,使用 Ramda
curry
将函数转换为柯里化变体:
const regularDistance = (x1, y1, x2, y2) => Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 );
const curriedDistance = R.curry(regularDistance);
console.log(regularDistance(1, 1, 4, 5)); // original
console.log(curriedDistance(1)(1)(4)(5)); // four 1 parameter functions
console.log(curriedDistance(1, 1)(4, 5)); // two 2 parameter functions
console.log(curriedDistance(1)(1)(4, 5)); // two 1 parameter functions + one 2 parameter
console.log(curriedDistance(1, 1, 4, 5)); // 4 parameter function
//etc
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.29.1/ramda.min.js"></script>