如何在 JavaScript 中使用柯里化和组合

636 阅读8分钟

「这是我参与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.everyArray.prototype.some使用相同的样式。回调针对数组的每个成员进行测试,回调接受一些值并返回 true 或 false。

让我们创建一些更有用的过滤器函数,但这次更高级一些。在这种情况下,我们可能想稍微“抽象”一下我们的函数,让我们让它们更易于重用。

例如,一些有用的函数可能是isEqualToisGreaterThan。它们更先进,因为它们需要两个值:一个定义为比较项(称为 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);

因此,前两个函数是我们的柯里化函数。他们需要一个参数,并返回一个函数,该函数反过来也需要一个参数。

基于这两个单参数函数,我们做一个简单的比较。后两个isSevenisOfLegalMajority是这两个函数的简单实现。

到目前为止,我们还没有变得复杂或参与其中,我们可以再保持小规模:

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

“在这种情况下,最终结果非常相似,所以它并没有真正为我们节省任何东西。事实上,考虑到前三个函数的设置,构建起来比简单地进行比较需要更多的时间!

这是真的。我不会争论。但它只是看到了一个更大的难题的一小部分。

  • 首先,我们正在编写更加自我记录的代码。通过使用富有表现力的函数名称,我们一眼就能看出我们正在过滤agesisTooYoungToRetire。我们没有看到数学,我们看到的是描述。
  • 其次,通过使用非常小的原子函数,我们能够单独测试每个部分,确保它每次都执行完全相同。稍后,当我们重用这些小函数时,我们可以确信它们会起作用——随着函数复杂性的增加,我们可以从测试每个小块中解放出来。
  • 第三,通过创建抽象函数,我们以后可能会在其他项目中找到它们的应用程序。构建功能组件库是一项非常强大的资产,我强烈建议培养它。  

话虽如此,我们还可以采用这些较小的功能,并开始将它们组合成越来越大的部分。现在让我们尝试一下:同时拥有isGreaterThanand 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 之间的偶数,我们只需将isEvenWAY 顶部的函数与isInRange使用 an的函数组合起来and,它就可以正常工作。

回顾

通过使用柯里化函数,我们能方便地组合函数。我们可以将一个函数的输出直接连接到下一个函数的输入,因为现在两者都采用一个参数。

通过使用组合,我们可以将较小的函数或柯里化函数组合成更大、更复杂的结构,并能够以最小的代价按预期工作。