「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。
柯里化的概念并不是一个新概念,但它非常有用。它也是函数式编程的基础,是一种以更模块化的方式思考函数的方式。
组合函数以创建更大、更复杂、更有用的函数为目的,看起来很直观,是函数式编程的关键组成部分。
当我们开始将它们结合起来时,就会发生一些有趣的事情。让我们看看这是如何工作的。 Curried 函数的作用与任何其他函数大致相同,但处理它们的方式有点不同。
假设我们想要一个可以检查两点之间距离的函数:{x1, y1}
和{x2, y2}
,例如。这个公式有点数学,但没有什么是我们无法处理的:
两点之间的距离公式,它是勾股定理的应用。
通常,调用我们的函数可能看起来像:
const distance = (start, end) => Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2) );
console.log( distance( {x:2, y:2}, {x:11, y:8} );
// logs 10.816653826391969
现在,currying 一个函数是强制它一次接受一个参数。因此,与其将其称为 ,我们不如这样distance( start, end )
称呼它:distance(start)(end)
。每个参数都是单独传入的,每个函数调用都返回另一个函数,直到提供了所有参数。
它可能比解释更容易,所以让我们看一下上面的距离函数作为一个柯里化函数:
const distance = function(start){
// we have a closed scope here, but we'll return a function that
// can access it - effectively creating a "closure".
return function(end){
// now, in this function, we have everything we need. we can do
// the calculation and return the result.
return Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2) );
}
}
console.log( distance({x:2, y:2})({x:11, y:8});
// logs 10.816653826391969 again
要获得相同的结果似乎需要做很多工作!我们可以通过使用 ES6 箭头函数来缩短它:
const distancewithCurrying =
(start) =>
(end) => Math.sqrt( Math.pow(end.x-start.x, 2) +
Math.pow(end.y-start.y, 2) );
为可读性添加缩进,不影响可运行性
但是再一次,除非我们开始以更抽象的方式思考我们的功能,否则似乎很多喧嚣没有真正的收获。
请记住,函数只能返回一件事。虽然我们可以提供任意数量的参数,但我们只会返回一个值,无论是数字、数组、对象还是函数。我们只拿回一件事。现在,有了柯里化函数,我们就有了一个只能接收一件事的函数。那里可能有联系。
碰巧的是,柯里化函数的强大之处在于能够组合和组合它们。
考虑我们的距离公式——如果我们正在编写一个“夺旗”游戏,那么快速轻松地计算每个玩家与旗帜的距离可能会很有用。我们可能有一组玩家,每个玩家都包含一个{x, y}
位置。有了一组{x,y}
值,一个可重用的函数就可以派上用场了。让我们玩一下这个想法:
const players = [
{
name: 'Alice',
color: 'aliceblue',
position: { x: 3, y: 5}
},{
name: 'Benji',
color: 'goldenrod',
position: { x: -4, y: -4}
},{
name: 'Clarissa',
color: 'firebrick',
position: { x: -2, y: 8}
}
];
const flag = { x:0, y:0};
这是我们的设置:我们有一个起始位置,flag
我们有一组玩家。我们定义了两个不同的函数来计算差异,让我们看看差异:
// Given those, let's see if we can find a way to map
// out those distances! Let's do it first with the first
// distance formula.
const distances = players.map( player => distance(flag, player.position) );
/***
* distances == [
* 5.830951894845301,
* 5.656854249492381,
* 8.246211251235321
* ]
***/
// using a curried function, we can create a function that already
// contains our starting point.
const distanceFromFlag = distanceWithCurrying(flag);
// Now, we can map over our players to extract their position, and
// map again with that distance formula:
const curriedDistances = players.map(player=>player.position)
.map(distanceFromFlag)
/***
* curriedDistances == [
* 5.830951894845301,
* 5.656854249492381,
* 8.246211251235321
* ]
***/
所以在这里,我们使用我们的distanceCurried
函数来应用一个参数,即起点。这返回了一个函数,该函数接受另一个参数,即终点。通过映射玩家,我们可以创建一个只包含我们需要的数据的新数组,然后将这些数据传递给我们的柯里化函数!
这是一个强大的工具,可能需要一些时间来适应。但是通过创建柯里化函数并将它们与其他函数组合,我们可以从更小、更简单的部分创建一些非常复杂的函数。
如何编写柯里化函数
能够映射 curried 函数非常有用,但您也会发现它们还有其他很好的用途。这是“函数式编程”的开始:编写小的、纯函数,这些函数可以像这些原子位一样正常执行,然后像构建块一样组合它们。
让我们看看如何使用柯里化函数,并将它们组合成更大的函数。下一个探索将进入过滤器功能。
首先,一点基础。Array.prototype.filter()
,ES6 过滤函数,允许我们定义一个回调函数,它接受一个或多个输入值并基于该值返回真或假。这是一个例子:
// a source array,
const ages = [11, 14, 26, 9, 41, 24, 108];
// a filter function. Takes an input, and returns true/false from it.
function isEven(num){
if(num%2===0){
return true;
} else {
return false;
}
}
// or, in ES6-style:
const isEven = (num) => num%2===0 ? true : false;
// and applied:
console.log( ages.filter(isEven) );
// [14, 26, 24, 108]
过滤一组数字以获得偶数
现在过滤器函数 ,isEven
以一种非常具体的方式编写:它接受一个值(或多个值,例如,如果我们想包含数组的索引),执行某种内部 hoojinkery,并返回 true 或 false。每次。
这是“过滤器回调函数”的本质,尽管它不是过滤器所独有的 -Array.prototype.every
和Array.prototype.some
使用相同的样式。回调针对数组的每个成员进行测试,回调接受一些值并返回 true 或 false。
让我们创建一些更有用的过滤器函数,但这次更高级一些。在这种情况下,我们可能想稍微“抽象”一下我们的函数,让我们让它们更易于重用。
例如,一些有用的函数可能是isEqualTo
或isGreaterThan
。它们更先进,因为它们需要两个值:一个定义为比较项(称为 a comparator
),另一个来自被比较的数组(我们将其称为value
)。这是更多代码:
// we write a function that takes in a value...
function isEqualTo(comparator){
// and that function *returns* a function that takes a second value
return function(value){
// and we simply compare those two values.
return value === comparator;
}
}
// again, in ES6:
const isEqualTo = (comparator) => (value) => value === comparator;
从这一点开始,我将坚持使用 ES6 版本,除非有特别具有挑战性的理由将代码扩展到经典版本。继续:
const isEqualTo = (comparator) => (value) => value === comparator;
const isGreaterThan = (comparator) => (value) => value > comparator;
// and in application:
const isSeven = isEqualTo(7);
const isOfLegalMajority = isGreaterThan(18);
因此,前两个函数是我们的柯里化函数。他们需要一个参数,并返回一个函数,该函数反过来也需要一个参数。
基于这两个单参数函数,我们做一个简单的比较。后两个isSeven
和isOfLegalMajority
是这两个函数的简单实现。
到目前为止,我们还没有变得复杂或参与其中,我们可以再保持小规模:
// function to simply invert a value: true <=> false
const isNot = (value) => !value;
const isNotEqual = (comparator) => (value) => isNot( isEqual(comparator)(value) );
const isLessThanOrEqualTo = (comparator) => (value) => isNot( isGreaterThan(comparator)(value) );
在这里,我们有一个效用函数,它简单地反转一个值的真实性,isNot
。使用它,我们可以开始组合更大的部分:我们获取比较器和值,通过isEqual
函数运行它们,然后我们isNot
将这个值说出来isNotEqual
。
这是作曲的开始,让我们公平地说——它看起来非常愚蠢。写下所有这些来得到这个有什么可能的用途:
// all of the building blocks...
const isGreaterThan = (comparator) => (value) => value > comparator;
const isNot = (value) => !value;
const isLessThanOrEqualTo = (comparator) => (value) => isNot( isGreaterThan(comparator)(value) );
// simply to get this?
const isTooYoungToRetire = isLessThanOrEqualTo(65)
// and in implementation:
const ages = [31, 24, 86, 57, 67, 19, 93, 75, 63];
console.log(ages.filter(isTooYoungToRetire)
// is that any cleaner than:
console.log(ages.filter( num => num <= 65 ) )
“在这种情况下,最终结果非常相似,所以它并没有真正为我们节省任何东西。事实上,考虑到前三个函数的设置,构建起来比简单地进行比较需要更多的时间! ”
这是真的。我不会争论。但它只是看到了一个更大的难题的一小部分。
- 首先,我们正在编写更加自我记录的代码。通过使用富有表现力的函数名称,我们一眼就能看出我们正在过滤
ages
值isTooYoungToRetire
。我们没有看到数学,我们看到的是描述。 - 其次,通过使用非常小的原子函数,我们能够单独测试每个部分,确保它每次都执行完全相同。稍后,当我们重用这些小函数时,我们可以确信它们会起作用——随着函数复杂性的增加,我们可以从测试每个小块中解放出来。
- 第三,通过创建抽象函数,我们以后可能会在其他项目中找到它们的应用程序。构建功能组件库是一项非常强大的资产,我强烈建议培养它。
话虽如此,我们还可以采用这些较小的功能,并开始将它们组合成越来越大的部分。现在让我们尝试一下:同时拥有isGreaterThan
and isLessThan
,我们可以编写一个不错的isInRange
函数!
const isInRange = (minComparator)
=> (maxComparator)
=> (value) => isGreaterThan(minComparator)(value)
&& isLessThan(maxComparator)(value)
const isTwentySomething = isInRange(19)(30);
太好了——我们现在有一种方法可以一次测试多个条件。但是从这个角度来看,它似乎并不是很自我记录。中间&&
并不可怕,但我们可以做得更好。
也许如果我们要编写另一个函数,我们可以调用and()
. 该and
函数可以采用任意数量的条件,并根据给定值测试它们。这将是有用的,可扩展的。
const and = (conditions) =
(value) => conditions.every(condition => condition(value) )
const isInRange = (min)
=>(max)
=> and([isGreaterThan(min), isLessThan(max) ])
因此,该and
函数采用任意数量的过滤器函数,并且仅当它们针对给定值都为真时才返回真。最后一个isInRange
函数的作用与前一个函数完全相同,但它似乎更具可读性和自我记录性。
此外,它将允许我们组合任意数量的函数:假设我们想要获得 20 到 40 之间的偶数,我们只需将isEven
WAY 顶部的函数与isInRange
使用 an的函数组合起来and
,它就可以正常工作。
回顾
通过使用柯里化函数,我们能方便地组合函数。我们可以将一个函数的输出直接连接到下一个函数的输入,因为现在两者都采用一个参数。
通过使用组合,我们可以将较小的函数或柯里化函数组合成更大、更复杂的结构,并能够以最小的代价按预期工作。