javascriptes6函数式编程:柯里化偏应用组合管道(代码片段)

瓜牛 瓜牛     2022-12-23     728

关键词:

上一篇介绍了闭包和高阶函数,这是函数式编程的基础核心。这一篇来看看高阶函数的实战场景。

首先强调两点:

  • 注意闭包的生成位置,清楚作用域链,知道闭包生成后缓存了哪些变量
  • 高阶函数思想:以变量作用域作为根基,以闭包为工具来实现各种功能

柯里化(curry)

定义:柯里化是把一个多参数函数转换为一个嵌套的一元函数的过程

先看个简单的例子,这是一个名为 add 的函数:const add = (x, y) => x + y;调用该函数 add(1, 1)、add(1, 2)、add(1, 3)...很普通,缺乏灵活性。

下面是柯里化实现版本:

const addCurried = x => y => x + y;

如果我们用一个单一的参数调用 addCurried,const add1 = addCurried(1)它返回一个函数fn = y => 1 + y,在其中 x 值通过闭包缓存下来。接下来,我们继续传参add1(1); add1(2); add1(3),有没有感觉比上面的 add 灵活。

上面的实现只是针对接收两个参数相加的柯里化函数,接下来正是开始实现个基础的通用的接收两个参数的柯里化函数:

const curry = (binaryFn) => 
    return function (firstArg) 
        return function (secondArg) 
            return binaryFn (firstArg, secondArg) ;  // 为啥要嵌套那么多呢?基于什么思路呢?思考一下...
        ;
    ;
;

现在可以用如下方式通过 curry 函数把 add 函数转换成一个柯里化版本:

const autoCurriedAdd = curry(add)
autoCurriedAdd(1)(1)  // 2

这里我们已经体会到柯里化的好处了,那么柯里化是怎样实现的呢?看上面 curry 的实现很容易发现,先传入一个接受二元函数,然后返回一个一元函数,当这个一元函数执行后,再返回一个一元函数,再次执行返回的一元函数时,触发最开始那个二元函数的执行。

这里有一个点很重要——执行时机,接收够两个参数(add 函数接收的参数数量)立即执行,也就是说接收够被柯里化函数的参数数量时触发执行

好的,我们已经实现了一个基础的柯里化函数。不过,这个 柯里化函数有很大的局限性——只能用于接收两个参数的函数。我们需要的是被柯里化函数的参数可以任意数量,怎么办呢?还好我们已经知道了被柯里化函数的执行时机——接收够被柯里化函数的参数数量时触发执行。下面我们来实现更复杂的柯里化:

// 柯里化函数
const curry = (fn) => 
  if (typeof fn !== \'function\') 
    throw Error(\'No function provided\')
  

  return function curriedFn (...args) 
    if (fn.length > args.length)   // 未达到触发条件,继续收集参数
      return function () 
        return curriedFn.apply(null, args.concat([].slice.call(arguments)))
      
    
    return fn.apply(null, args)
  

这样,我们就能处理多个参数的函数了。比如:

const multiply = (x, y, z) => x*y*z;

const curryMul = curry(multiply);
const result = curryMul(1)(2)(3); // 1*2*3 = 6

偏应用(partial)

偏应用,又称作部分应用,它允许开发者部分地应用函数参数。实际上,偏应用是为一个多元函数预先提供部分参数,从而在调用时可以省略这些参数

比如我们要在每10ms做一组操作。可以通过 setTimeout 函数以如下方式实现:

setTimeout( () => console.log("Do X task"), 10);
setTimeout( () => console.log("Do Y tash"), 10);

很显然,我们可以用上面的 curry 函数包装成柯里化函数,实现灵活调用:

// 实现一个二元函数,用于柯里化
const setTimeoutWrapper = (time, fn) => 
    setTimeout(fn, time);


// 使用 curry 函数封装 setTimeout 来实现一个10ms延迟
const delayTenMs = curry(setTimeoutWrapper)
delayTenMs( () => console.log("Do X task") );
delayTenMs( () => console.log("Do Y task") );

很棒,也能实现灵活调用。但问题是我们不得不创建 setTimeoutWrapper 一样的封装器,这也是一种开销。下面我们看看偏应用的实现:

// 偏应用函数
const partial = (fn, ...partialArgs) => 
  let args = partialArgs
  return (...fullArguments) => 
    let count = 0
    for (let i = 0; i < args.length && count < fullArguments; i++) 
      if (args[i] === undefined) 
        args[i] = fullArguments[count++]
      
    
    return fn.apply(null, args)
  

下面用偏应用解决上面的延时10ms问题:

let delayTenMs = partial(setTimeout, undefined, 10);  // 注意此处,让我们少创建了一个 setTimeoutWrapper 封装器
delayTenMs( () => console.log("Do X task") )
delayTenMs( () => console.log("Do Y task") );

现在我们对柯里化有了更清晰的认识。创建偏应用函数时,第一个参数接收一个函数,剩余参数是第一个传入函数所需参数。剩余参数待传入的用undefined占位,执行偏应用函数时填充undefined

组合(compose)

在了解什么是函数式组合之前,让我们理解组合的概念。

符合“|”被称为管道,它允许我们通过组合一些函数去创建一个能够解决问题的新函数。大致来说,“|”将最左侧的函数输出作为输入发送给最右侧的函数!从技术上讲,该处理过程称为“管道”。

compose 函数:

const compose = (a, b) => (c) => a(b(c))

compose 函数会首先执行 b 函数,并将 b 的返回值作为参数传递给 a。该函数调用的方向是从右至左的(先执行 b,再执行 a)。

可以看到,组合函数 compose 就是传入一些函数。对于传入的函数,我们要求一个函数只做一件事

下面看下如何应用 compose 函数:

// 通过组合计算字符串单词个数
let splitIntoSpaces = (str) => str.split(" ");   // 分割成数组
let count = (array) => array.length;  // 计算长度

const countWords = compose(count, splitIntoSpaces);

countWord("hello your reading about composition"); // 5

上面的 compose 只能实现两个函数的组合。如何组合更多个函数呢?这就需要借助reduce的威力了:

// 组合多个函数 composeN
const composeN = (...fns) => 
    (value) => 
        fns.reverse().reduce((acc, fn) => fn(acc), value);

管道/序列(pipe)

管道和组合的概念很类似,都是串行处理数据。唯一区别就是执行方向:组合从右向左执行,管道从左向右执行。

// 组合多个函数 pipe
const pipe= (...fns) => 
    (value) => 
        fns.reduce((acc, fn) => fn(acc), value);

下面看下如何应用 pipe 函数:

// 通过管道计算字符串单词个数
let splitIntoSpaces = (str) => str.split(" ");   // 分割成数组
let count = (array) => array.length;  // 计算长度

const countWords = pipe(splitIntoSpaces, count);  // 注意此处的传参顺序

countWord("hello your reading about composition"); // 5

总结

通过这一节的学习,我们知道了高阶函数的一些应用——柯里化、偏应用、组合和管道,每种应用都有特定的应用场景。

其中,柯里化是最常用的一种场景,它的作用是把一个多参数函数转换为一个嵌套的一元函数的过程。随着闭包的产生,我们可以灵活的调用。

组合和管道类似,都是串行处理数据。传入一个初始数据,通过一系列特定顺序的纯函数处理成我们希望得到的数据。

参考链接:
简明 JavaScript 函数式编程——入门篇

javascript函数式编程-包含闭包链式优化及柯里化

本文着重介绍个人理解的函数式编程。函数式编程个人理解为:以函数为主要载体的编程方式。好处:语义更加清晰可复用性高可维护性好作用域局限、副作用少基本函数式编程://实现数组中每个单词首字母大写//一般写法const... 查看详情

函数式编程:纯函数&柯里化&组合函数(代码片段)

函数式编程:纯函数&柯里化&组合函数纯函数函数的柯里化组合函数纯函数相同的输入值,产生相同的输出在函数的执行过程中,不能产生副作用。不能对传入的参数进行修改,不依赖上层作用域内的数据。... 查看详情

函数式编程之柯里化(curry)

 函数式编程curry的概念:只传递给函数一部分参数来调用函数,然后返回一个函数去处理剩下的参数。varadd=function(x){returnfunction(y){returnx+y;};};varincrement=add(1);increment(1)//2varaddTen=add(10);addTen(10)//20我们可以一次性的调用函数,也... 查看详情

#yyds干货盘点#js函数式编程:柯里化

面向对象编程和函数式编程是两种非常不同的编程范式,它们有自己的规则和优缺点。但是,JavaScript并没有一直遵循一个规则,而是正好处于这两个规则的中间,它提供了普通OOP语言的一些方面,比如类、对象、继承等等。但... 查看详情

函数式编程-函数的合成与柯里化

函数式编程有两个最基本的运算:合成和柯里化。2.1函数的合成如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。上图中,X和Y之间的变形关系是函数f,... 查看详情

函数式编程之一柯里化(代码片段)

什么是柯里化?柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数的函数 下面来看一个案例,两值相加:柯里化之前(常见用法)functionadd(x,y)returnx+yadd(1,2)柯里化之后functionadd(x)returnfunction(y)returnx+yadd(1)(2)从... 查看详情

理解函数式编程

相信大家平时或多或少听过不少关于“函数式编程”(FP)相关的词语,有些Geek经常吹捧函数式的优点或者特性比如:纯函数无副作用、不变的数据、高阶函数、流计算模式、尾递归、柯里化等等,再加上目前的函数式理论越来... 查看详情

javascript系列:函数式编程(开篇)

前言:上一篇介绍了函数回调,高阶函数以及函数柯里化等高级函数应用,同时,因为正在学习JavaScript·函数式编程,想整理一下函数式编程中,对于我们日常比较有用的部分。 为什么函数式编程很重要?   学习... 查看详情

swift柯里化(代码片段)

...化(Currying),又称部分求值(PartialEvaluation),是一种函数式编程思想,就是把接受多个参数的函数转换成接收一个单一参数(最初函数的第一个参数)的函数,并且返回一个接受余下参数的新函数技术。uncurried:普通函 查看详情

分部应用

分部应用带入函数中的一些参数在上一篇关于柯里化的文章中,我们了解它将多参数函数分为较小的一个参数函数。这是数学上一种正确的方式,但是不是它可以运行的唯一原因-它也导致出现一个叫做分部函数应用(partialfunctiona... 查看详情

纯函数式编程

】纯函数式编程【英文标题】:PurelyFunctionalProgramming【发布时间】:2016-11-2818:46:51【问题描述】:所以,我是一位经验丰富的OOP程序员(主要是C++),现在刚刚开始涉足函数式编程。据我了解,在纯函数范式中,函数不应该有条... 查看详情

你真的理解函数式编程吗?

你真的理解函数式编程吗? 大数据以及人工智能越来越流程,你是否可以轻松适应大数据编程,函数式编程在其中起着重要作用,如何从面向对象编程跳槽到函数式编程?你是否觉得函数式各种概念难于理解?本场Chat将为... 查看详情

柯里化

柯里化将多参数函数分为较小的一个参数函数在一些基本类型的题外话之后,我们再次回到函数上,特别是我们前面提到的难题,如果数学函数只能有一个参数,那么F#的函数怎么能有多个参数?答案很简单:多参数的函数被重... 查看详情

从延迟处理讲起,javascript也能惰性编程?

前文回顾​​#✨从历史讲起,JavaScript基因里写着函数式编程​​​​#✨从柯里化讲起,一网打尽JavaScript重要的高阶函数​​​​#✨从纯函数讲起,一窥最深刻的函子Monad​​我们从闭包起源开始、再到百变柯里化等一票高阶... 查看详情

onjava8第十三章函数式编程

...2.1递归3方法引用3.1Runnable接口3.2未绑定的方法引用3.3构造函数引用4函数式接口4.1多参数函数式接口4.2缺少基本类型的函数5高阶函数6闭包6.1作为闭包的内部类7函数组合8柯里化和部分求值9纯函数式编程10本章小结函数式编程的中... 查看详情

从柯里化讲起,一网打尽javascript重要的高阶函数

...顾我们在前篇​​《✨从历史讲起,JavaScript基因里写着函数式编程》​​讲到了JavaScript的函数式基因最早可追溯到1930年的lambda运算,这个时间比第一台计算机诞生的时间都还要早十几年。JavaScript闭包的概念也来源于lambda运算... 查看详情

前端学习之函数式编程—函数组合(代码片段)

Part01函数组合纯函数和柯里化很容易写出洋葱代码=====>h(g(f(x)))一层一层套起来不宜阅读例如:获取数组最后的一个元素再转大写字母_.toUpper(_.first(_.reverse(array)))函数组合可以让我们把细粒度的函数重新组成... 查看详情

什么是函数式编程?(代码片段)

 纯函数:定义:对于相同的输入永远会得到相同的输出,而且没有任何可以观察的副作用,也不依赖外部的环境状态。 例如数学公式:y=f(x)在javascript中,对于数组的操作,有的是纯的,有的是不存的,如:letarr=[1,2,3,4,5... 查看详情