JS函数curry(柯里化)

2023-11-16

原文地址:http://blog.jobbole.com/77956/

什么是柯里化?

柯里化是这样的一个转换过程,把接受多个参数的函数变换成接受一个单一参数(译注:最初函数的第一个参数)的函数,如果其他的参数是必要的,返回接受余下的参数且返回结果的新函数。(译注1)

当我们这么说的时候,我想柯里化听起来相当简单。JavaScript中是怎么实现的呢?
假设我们要写一个函数,接受3个参数。

JavaScript
1
2
3
4
; html - script : false ]
var sendMsg = function ( from , to , msg ) {
     alert ( [ "Hello " + to + "," , msg , "Sincerely," , "- " + from ] . join ( "\n" ) ) ;
} ;

现在,假定我们有柯里化函数,能够把传统的JavaScript函数转换成柯里化后的函数:

JavaScript
1
2
3
4
5
6
7
; html - script : false ]
var sendMsgCurried = curry ( sendMsg ) ; // returns function(a,b,c)
 
var sendMsgFromJohnToBob = sendMsgCurried ( "John" ) ( "Bob" ) ; // returns function(c)
 
sendMsgFromJohnToBob ( "Come join the curry party!" ) ;
//=> "Hello Bob, Come join the curry party! Sincerely, - John"

手动柯里化

在上面的例子中,我们假定拥有神秘的curry函数。我会实现这样的函数,但是现在,我们首先看看为什么这样的函数是如此必要。
举个例子,手动柯里化一个函数并不困难,但是确实有点啰嗦:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; html - script : false ]
// uncurried
var example1 = function ( a , b , c ) {
     // do something with a, b, and c
} ;
 
// curried
var example2 = function ( a ) {
     return function ( b ) {
         return function ( c ) {
             // do something with a, b, and c
         } ;
     } ;
} ;

在JavaScript,即使你不指定一个函数所有的参数,函数仍将被调用。这是个非常实用JavaScript的功能,但是却给柯里化制造了麻烦。

思路是每一个函数都是有且只有一个参数的函数。如果你想拥有多个参数,你必须定义一系列相互嵌套的函数。讨厌!这样做一次两次还可以,可是需要以这种方式定义需要很多参数的函数的时候,就会变得相当啰嗦和难于阅读。(但是别担心,我会马上告诉你一个办法)

一些函数编程语言,像Haskell和OCaml,语法中内置了函数柯里化。在这些语言中,举个例子,每个函数是拥有一个参数的函数,并且只有一个参数。你可能会认为这种限制麻烦胜过好处,但是语言的语法就是这样,这种限制几乎无法察觉。

举个例子,在OCaml,你可以用两种方式定义上面example:

JavaScript
1
2
3
4
5
6
7
8
; html - script : false ]
let example1 = fun a b c ->
     // (* do something with a, b, c *)
 
let example2 = fun a ->
     fun b ->
         fun c ->
             // (* do something with a, b, c *)

很容易看出这两个例子和上面的那两个例子是如何的相似。

区别,然而,是否在OCaml也是做了同样的事情。OCaml,没有拥有多个参数的函数。但是,在一行中声明多个参数就是嵌套定义单参函数“快捷方式”。

类似的 ,我们期待调用柯里化函数句法上和OCaml中调用多参函数类似。我们期望这样调用上面的函数:

JavaScript
1
2
3
; html - script : false ]
example1 foo bar baz
example2 foo bar baz

而在JavaScript,我们采用明显不同的方式:

JavaScript
1
2
3
; html - script : false ]
example1 ( foo , bar , baz ) ;
example2 ( foo ) ( bar ) ( baz ) ;

在OCaml这类语言中,柯里化是内置的。在JavaScript,柯里化虽然可行(高阶函数),但是语法上是不方便的。这也是为什么我们决定编写一个柯里化函数来帮我们做这些繁琐的事情,并使得我们的代码简洁。

创建一个curry辅助函数

理论上我们期望可以有一个方便的方式转换普通老式的JavaScript函数(多个参数)到完全柯里化的函数。

这个想法不是我独有的,其他的人已经实现过了,例如在wu.js 库中的.autoCurry()函数(尽管你关心的是我们自己的实现方式)。

首先,让我们创建一个简单的辅助函数 .sub_curry:

JavaScript
1
2
3
4
5
6
7
; html - script : false ]
function sub_curry ( fn /*, variable number of args */ ) {
     var args = [ ] . slice . call ( arguments , 1 ) ;
     return function ( ) {
         return fn . apply ( this , args . concat ( toArray ( arguments ) ) ) ;
     } ;
}

让我们花点时间看看这个函数的功能。相当简单。sub_curry接受一个函数fn作为它的第一个参数,后面跟着任何数目的输入参数。返回的是一个函数,这个函数返回fn.apply执行结果,参数序列合并了该函数最初传入参数的,加上fn调用的时候传入参数的。

看例子:

JavaScript
1
2
3
4
5
6
7
8
9
; html - script : false ]
var fn = function ( a , b , c ) { return [ a , b , c ] ; } ;
 
// these are all equivalent
fn ( "a" , "b" , "c" ) ;
sub_curry ( fn , "a" ) ( "b" , "c" ) ;
sub_curry ( fn , "a" , "b" ) ( "c" ) ;
sub_curry ( fn , "a" , "b" , "c" ) ( ) ;
//=> ["a", "b", "c"]

很明显,这并不是我门想要的,但是看起来有点柯里化的意思了。现在我们将定义柯里化函数curry:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; html - script : false ]
function curry ( fn , length ) {
     // capture fn's # of parameters
     length = length || fn . length ;
     return function ( ) {
         if ( arguments . length < length ) {
             // not all arguments have been specified. Curry once more.
             var combined = [ fn ] . concat ( toArray ( arguments ) ) ;
             return length - arguments . length > 0
                 ? curry ( sub_curry . apply ( this , combined ) , length - arguments . length )
                 : sub_curry . call ( this , combined ) ;
         } else {
             // all arguments have been specified, actually call function
             return fn . apply ( this , arguments ) ;
         }
     } ;
}

这个函数接受两个参数,一个函数和要“柯里化”的参数数目。第二个参数是可选的,如果省略,默认使用Function.prototype.length 属性,就是为了告诉你这个函数定义了几个参数。

最终,我们能够论证下面的行为:

JavaScript
1
2
3
4
5
6
7
8
9
10
; html - script : false ]
var fn = curry ( function ( a , b , c ) { return [ a , b , c ] ; } ) ;
 
// these are all equivalent
fn ( "a" , "b" , "c" ) ;
fn ( "a" , "b" , "c" ) ;
fn ( "a" , "b" ) ( "c" ) ;
fn ( "a" ) ( "b" , "c" ) ;
fn ( "a" ) ( "b" ) ( "c" ) ;
//=> ["a", "b", "c"]

我知道你在想什么…

等等…什么?!

难道你疯了?应该是这样!我们现在能够在JavaScript中编写柯里化函数,表现就如同OCaml或者Haskell中的那些函数。甚至,如果我想要一次传递多个参数,我可以向我从前做的那样,用逗号分隔下参数就可以了。不需要参数间那些丑陋的括号,即使是它是柯里化后的。

这个相当有用,我会立即马上谈论这个,可是首先我要让这个Curry函数前进一小步。

柯里化和“洞”(“holes”)

尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。终究,柯里化的背后思路就是创建函数,更具体的功能,分离其他更多的通用功能,通过分步应用它们。

当然这个只能工作在当最左参数就是你想要分步应用的参数!

为了解决这个,在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如过作为一个函数的参数被传入,就表明这个是可以“跳过的”。是尚待指定的。

这是非常有用的,当你想要分步应用(partially apply)一个特定函数,但是你想要分布应用(partially apply)的参数并不是最左参数。

举个例子,我们有这样的一个函数:

JavaScript
1
2
; html - script : false ]
var sendAjax = function ( url , data , options ) { /* ... */ }

也许我们想要定义一个新的函数,我们部分提供SendAjax函数特定的Options,但是允许url和data可以被指定。

当然了,我们能够相当简单的这样定义函数:

JavaScript
1
2
3
4
; html - script : false ]
var sendPost = function ( url , data ) {
     return sendAjax ( url , data , { type : "POST" , contentType : "application/json" } ) ;
} ;

或者,使用使用约定的下划线方式,就像下面这样:

JavaScript
1
2
; html - script : false ]
var sendPost = sendAjax ( _ , _ , { type : "POST" , contentType : "application/json" } ) ;

注意两个参数以下划线的方式传入。显然,JavaScript并不具备这样的原生支持,于是我们怎样才能这样做呢?

回过头让我们把curry函数变得智能一点…

首先我们把我们的“占位符”定义成一个全局变量。

JavaScript
1
2
; html - script : false ]
var _ = { } ;

我们把它定义成对象字面量{},便于我们可以通过===操作符来判等。

不管你喜不喜欢,为了简单一点我们就使用_来做“占位符”。现在我们就可以定义新的curry函数,就像下面这样:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
; html - script : false ]
function curry ( fn , length , args , holes ) {
     length = length || fn . length ;
     args = args || [ ] ;
     holes = holes || [ ] ;
     return function ( ) {
         var _args = args . slice ( 0 ) ,
             _holes = holes . slice ( 0 ) ,
             argStart = _args . length ,
             holeStart = _holes . length ,
             arg , i ;
         for ( i = 0 ; i < arguments . length ; i ++ ) {
             arg = arguments [ i ] ;
             if ( arg === _ && holeStart ) {
                 holeStart -- ;
                 _holes . push ( _holes . shift ( ) ) ; // move hole from beginning to end
             } else if ( arg === _ ) {
                 _holes . push ( argStart + i ) ; // the position of the hole.
             } else if ( holeStart ) {
                 holeStart -- ;
                 _args . splice ( _holes . shift ( ) , 0 , arg ) ; // insert arg at index of hole
             } else {
                 _args . push ( arg ) ;
             }
         }
         if ( _args . length < length ) {
             return curry . call ( this , fn , length , _args , _holes ) ;
         } else {
             return fn . apply ( this , _args ) ;
         }
     }
}

实际代码还是有着巨大不同的。 我们这里做了一些关于这些“洞”(holes)参数是什么的记录。概括而言,运行的职责是相同的。

展示下我们的新帮手,下面的语句都是等价的:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; html - script : false ]
var f = curry ( function ( a , b , c ) { return [ a , b , c ] ; } ) ;
var g = curry ( function ( a , b , c , d , e ) { return [ a , b , c , d , e ] ; } ) ;
 
// all of these are equivalent
f ( "a" , "b" , "c" ) ;
f ( "a" ) ( "b" ) ( "c" ) ;
f ( "a" , "b" , "c" ) ;
f ( "a" , _ , "c" ) ( "b" ) ;
f ( _ , "b" ) ( "a" , "c" ) ;
//=> ["a", "b", "c"]
 
// all of these are equivalent
g ( 1 , 2 , 3 , 4 , 5 ) ;
g ( _ , 2 , 3 , 4 , 5 ) ( 1 ) ;
g ( 1 , _ , 3 ) ( _ , 4 ) ( 2 ) ( 5 ) ;
//=> [1, 2, 3, 4, 5]

疯狂吧?!

我为什么要关心?柯里化能够怎么帮助我?

你可能会停在这儿思考…

这看起来挺酷而且…但是这真的能帮助我编写更好的代码?

这里有很多原因关于为什么函数柯里化是有用的。

函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。

为了给一个简单的例子,让我们分别使用Vanilla.js, Underscore.js, and “函数化方式” (极端利用函数化特性)来编写CSV解析器。

Vanilla.js (Imperative)

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; html - script : false ]
//+ String -> [String]
var processLine = function ( line ) {
     var row , columns , j ;
     columns = line . split ( "," ) ;
     row = [ ] ;
     for ( j = 0 ; j < columns . length ; j ++ ) {
         row . push ( columns [ j ] . trim ( ) ) ;
     }
} ;
 
//+ String -> [[String]]
var parseCSV = function ( csv ) {
     var table , lines , i ;   
     lines = csv . split ( "\n" ) ;
     table = [ ] ;
     for ( i = 0 ; i < lines . length ; i ++ ) {
         table . push ( processLine ( lines [ i ] ) ) ;
     }
     return table ;
} ;

Underscore.js

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
; html - script : false ]
//+ String -> [String]
var processLine = function ( row ) {
     return _ . map ( row . split ( "," ) , function ( c ) {
         return c . trim ( ) ;
     } ) ;
} ;
 
//+ String -> [[String]]
var parseCSV = function ( csv ) {
     return _ . map ( csv . split ( "\n" ) , processLine ) ;
} ;

函数化方式

JavaScript
1
2
3
4
5
6
; html - script : false ]
//+ String -> [String]
var processLine = compose ( map ( trim ) , split ( "," ) ) ;
 
//+ String -> [[String]]
var parseCSV = compose ( map ( processLine ) , split ( "\n" ) ) ;

所有这些例子功能上是等价的。我有意的尽可能的简单的编写这些。

想要达到某种效果是很难的,但是主观上这些例子,我真的认为最后一个例子,函数式方式的,体现了函数式编程背后的威力。

关于curry性能的备注

一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?

通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。

有关性能,这里有一些事情必须牢记于心:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

在大多是web应用中,“瓶颈”会发生在操控DOM上。这是非常不可能的,你在所有方面关注性能。显然,用不用上面的代码自行考虑。

接下来:

我已经讨论了不少,关于在JavaScript应用函数式编程。接下来,在这个系列我们会讨论可变参数函数,Functors, Monads, 和其它的。

同时,我被要求提供关于函数式编程更深层次的例子,为了这个我将会实现病毒拼图游戏puzzle-game 2048,并且从零开始实现人工智能规划求解AI Solver!别“换台”啊!:-)

译注1:通常,柯里化是这样的过程,“如果你固定某些参数,你将得到接受余下参数的一个函数”。所以对于有两个变量的函数y^x,如果固定了 y=2,则得到有一个变量的函数 2^x

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JS函数curry(柯里化) 的相关文章

随机推荐

  • Zookeeper和Nacos的区别

    目录 Zookeeper 1 ZK结构 2 ZK的消息广播和崩溃恢复 Nacos 1 存储和数据更新 2 注册中心 Zookeeper 1 ZK结构 Zookeeper的功能主要是通过它的树形节点来实现的 当有节点数据变化时或者说节点过期的
  • Dropout Learning - 防止深度神经网络过拟合

    最近在学习caffe 里面有一个名词叫做Dropout Learning 一直没明白是什么意思 直到最近才发现一片文章介绍Dropout Learning的 希望可以给不知道的同学一定的帮助 如果想要更深入的了解可以阅读该文献 文章结尾会给
  • MOV指令在32位汇编程序和64位汇编程序下的相同与不同之处

    mov指令原则 两个操作数 目标操作数和源操作数 的大小必须相同 两个操作数不能同时为内存操作数 也就是不能内存 到 内存 指令指针寄存器不能作为目标操作数 64位汇编程序下 32位汇编程序和64位汇编程序都依照上面的规则 语法也相同 但如
  • 目标跟踪算法

    目标跟踪算法 一 目标跟踪算法简介 1 1 主要任务 1 1 1 Online Visual Tracker BenchMark 1 1 2 VOT 1 2 难点与挑战 1 3 分类 1 3 1 常规分类 1 3 2 时间分类 二 常用算法
  • 从Bengio的NPS模型看AGI的实现通路

    来源 混沌巡洋舰 这两天深度学习祖师Yoshua Bengio 的 Neural Production System 刷新了AI圈子 与以往的深度学习套路不同的是 这篇文章有效的把符号主义AI对人类认知的模拟与深度学习结合 得到了一个能够学
  • 《每日一题》NO.21:画出CMOS 非门/与非门/或非门的结构

    芯司机 每日一题 会每天更新一道IC面试笔试题 其中有些题目已经被很多企业参考采用了哦 聪明的你快来挑战一下吧 今天是第21题 CMOS Complementary Metal Oxide Semiconductor 互补金属氧化物半导体
  • Vivo 2019秋季校园招聘笔试题(9月22号机考)

    Vivo笔试题这次真是出乎意料了 上来就直接三道编程题奉上 题目描述 1 小V在公司负责游戏运营 今天收到一款申请新上架的游戏 跳一跳 为了确保提供给广大玩家朋友们的游戏都是高品质的 按照运营流程小V必须对新游戏进行全方位了解体验和评估 这
  • webpack多个Html,Webpack构建多页面应用配置

    安装依赖 devDependencies babel core 7 12 3 babel preset env 7 12 1 babel preset react 7 12 1 babel loader 8 1 0 clean webpac
  • 一元多项式的相加

    一元多项式的表达和相加 使用单链表表示一元多项式 由于使用java语言自己编写实现 没有使用LinkedList集合实现 所以需要创建单链表的类 类中包含指数 系数和后继元素的地址 类的设计如下 public class SingleLis
  • 01背包问题(采药) 动态规划,python实现

    采药问题 题目描述 辰辰是个天资聪颖的孩子 他的梦想是成为世界上最伟大的医师 为此 他想拜附近最有威望的医师为师 医师为了判断他的资质 给他出了一个难题 医师把他带到一个到处都是草药的山洞里对他说 孩子 这个山洞里有一些不同的草药 采每一株
  • python后端接口框架Flask的基本用法

    简介 在现代Web开发中 后端接口是十分重要的一部分 它们建立了前端和后端之间的连接 使得数据能够在两者之间传递 Python是一门受欢迎的动态编程语言 它可以用来编写高效且功能强大的后端接口 本文将介绍如何使用Python编写后端接口 以
  • WSS 代码执行的权限提升

    WSS 代码执行的权限提升 概述 WSS 默认使用身份模拟执行代码 也就是说用当前登录的用户身份执行Web Part或者自定义应用程序的代码访问 在大多数情况下 这种机制能够准确并严格地控制了标准权限的用户他对特定网站资源和敏感数据的访问
  • [LeetCode-35]-Search Insert Position(搜索整数所在的位置)

    文章目录 题目相关 Solution 1 顺序遍历 2 算法改进 二分查找 题目相关 题目解读 从有序整数列表中搜索给定整数 如果在其中返回下标位置 如果不在 返回应该在的位置 题目 原题链接 Given a sorted array an
  • MyBatis3 映射boolean 类型注意事项

    1 MySQL8 数据库关于boolean 存储结构定义 使用tinyint 1 代表Boolean 类型 2 实体定义关于属性字段为boolean 类型定义 3 实体属性与数据库字段映射文件配置 Mapper xml 文件 4 控制层 如
  • 你不知道的JavaScript---异步:现在与未来

    目录 异步 分块的程序 事件循环 并行线程 并发 非交互 交互 协作 任务 语句顺序 异步 js如何表达和控制持续一段时间的程序行为 分散在一段时间内运行的程序行为 持续一段时间 不是指类似于 for循环开始到结束的过程 而是指 程序的一部
  • Android 嵌套滑动总结,android基础考试题及答案

  • git 合并多个 commit

    1 rebase 介绍 rebase在git中是一个非常有魅力的命令 使用得当会极大提高自己的工作效率 相反 如果乱用 会给团队中其他人带来麻烦 它的作用简要概括为 可以对某一段线性提交历史进行编辑 删除 复制 粘贴 因此 合理使用reba
  • 数据分析(一)

    label distribution 是一个不均衡的数据集 需要做数据预处理 Sentence length distribution 句子的长度也很极端 有很多的outliers 需要对过长的数据进行舍弃或者切割
  • Eviews用向量自回归模型VAR实证分析公路交通通车里程与经济发展GDP协整关系时间序列数据和脉冲响应可视化

    最近我们被客户要求撰写关于向量自回归模型的研究报告 包括一些图形和统计输出 视频 向量自回归VAR数学原理及R软件经济数据脉冲响应分析实例 视频 向量自回归VAR数学原理及R语言软件经济数据脉冲响应分析实例 时长12 01 河源市是国务院1
  • JS函数curry(柯里化)

    原文地址 http blog jobbole com 77956 什么是柯里化 柯里化是这样的一个转换过程 把接受多个参数的函数变换成接受一个单一参数 译注 最初函数的第一个参数 的函数 如果其他的参数是必要的 返回接受余下的参数且返回结果