JS函数式编程【译】4.1 部分函数应用和珂理化

?? Functional Programming in Javascript 主目录第四章 在Javascript中实现函数式编程的技术

部分函数应用和珂理化

许多语言支持可选参数,但是Javascript不支持。Javascript采用一种完全不同的模式,它任允许意数量的参数传给函数。
这就给一些有趣且非同寻常的设计模式留下了门路。函数可以全部或部分应用。

部分应用在Javascript中的处理方式是:给函数的一个或多个参数绑定上值,然后返回另一个函数接受剩余的未绑定参数。
同样,珂理化的处理方式是把一个有多个参数的函数转换为一个只接受一个参数的函数,它返回的函数接受剩余的参数。

这两者的差异现在看起来不是很明显,但最后会清楚的。

函数操作

在我们进一步解释如何实现部分应用和珂理化之前,我们需要进行一些回顾。如果我们想要扒掉JavaScript厚重的C风格语法外衣,
暴露其函数式本质的话,我们需要理解原始函数、原型在JavaScript中是如何工作的;而如果我们只是想设置一些cookie
或验证一些表单的话则永远不用考虑这些。

apply、call和this关键词

在纯函数式语言中,函数不会被唤起(invoke),他们是被应用(apply)。JavaScript以同样的方式工作,
甚至提供了手动调用(call)和应用(apply)函数的工具。这写都是与this关键词有关的,当然this指的是函数所属的那个对象。

call()函数把第一个参数作为this关键字。它是这样工作的:

console.log([‘Hello‘, ‘world‘].join(‘ ‘)) // 正常方式
console.log(Array.prototype.join.call([‘Hello‘, ‘world‘], ‘ ‘)); //使用call

call()函数可以唤起匿名函数:

console.log((function(){console.log(this.length)}).call([1,2,3]));

apply()函数和call()函数很像,但是更有用一些:

console.log(Math.max(1,2,3)); // 返回3
console.log(Math.max([1,2,3])); // 无法应用于数组
console.log(Math.max.apply(null, [1,2,3])); // 这样就可以了

基本的区别是:call()函数接受一列参数,apply函数接受一个数组作为参数。

call()和apply()让你可以只写一次函数,其它对象可以继承它而无需再写一遍函数。
并且他俩都是Function对象的成员。

当你对call()自己调用call()的时候,会发生些有趣的事情。

// 这两行代码是等价的
func.call(thisValue);
Function.prototype.call.call(func, thisValue);

绑定参数

bind()函数让你能够调用一个对象的函数时this指向另一个对象。这跟call()函数差不多,
不过它可以让方法链式调用,返回一个新的函数。

这对于回调非常有用,就像下面的代码那样:

function Drum() {
  this.noise = ‘boom‘;
  this.duration = 1000;
  this.goBoom = function() {
    console.log(this.noise)
  };
}
var drum = new Drum();
setInterval(drum.goBoom.bind(drum), drum.duration);

这解决了许多面向对象框架中的问题,比如Dojo,特别是对于那些有自己的handler函数的类处理状态维持的问题。
不过我们也可以用bind()来进行函数式编程。

bind()函数实际上自己实现了部分应用,尽管是通过一种很有限的方式。

函数工厂

还记得第二章《函数式编程基础》中关于闭包的那节吗?闭包使建立函数工厂这种Javascript编程模式成为可能。
它们使你能够手动绑定函数的参数。

首先我们需要一个为另一个函数绑定参数的函数:

function bindFirstArg(func, a) {
  return function(b) {
    return func(a, b);
  };
}

现在我们可以用它来创建更多的泛型函数(generic function):

var powersOfTwo = bindFirstArg(Math.pow, 2);
console.log(powersOfTwo(3)); // 8
console.log(powersOfTwo(5)); // 32

也可以针对于其它参数:

function bindSecondArg(func, b) {
  return function(a) {
    return func(a, b);
  };
}
var squareOf = bindSecondArg(Math.pow, 2);
var cubeOf = bindSecondArg(Math.pow, 3);
console.log(squareOf(3)); // 9
console.log(squareOf(4)); // 16
console.log(cubeOf(3)); // 27
console.log(cubeOf(4)); // 64

在函数式编程中,创建泛型函数的能力十分重要。然而还有更巧妙的方式可以更加一般化地完成这一过程。
bindFirstArg()函数接受两个参数,第一个参数是个函数。如果我们把bindFirstArg本身作为第一个参数的函数传给它自己,
我们就可以创建绑定函数。最好用下面的例子来描述:

var makePowersOf = bindFirstArg(bindFirstArg, Math.pow);
var powersOfThree = makePowersOf(3);
console.log(powersOfThree(2)); // 9
console.log(powersOfThree(3)); // 27

这就是为什么它被叫做函数工厂。

部分应用

注意我们函数工厂的例子里bindFirstArg()和bindSecondArg()函数只能有两个参数。
我们可以写新的不同数量参数的函数,但是这就违背我们一般化的模型了。

我们需要部分应用

部分应用是这样一个过程:它给函数的一个或多个参数绑定上值,返回一个已经部分应用过的函数,
这个函数仍然需要接受未绑定的参数。

与bind()函数等Fuction对象内建的方法不同,我们需要创建自己的函数来实现部分调用和柯里化。
主要有两种方式:

  • 作为一个单独的函数,也就是,var partial = function(func){...
  • 作为补充,也就是,Function.prototype.partial = function(){...

补充的方式是为原型增加新的函数,这会允许我们在为想要部分应用的函数调用我们的新函数的时候作为它的一个方法。
就像这样:myfunction.partial(arg1, arg2, ...);

左端部分应用

这里Javascript的call()和apply()函数将对我们很有用。我们来看看补充Function对象的方式:

Function.prototype.partialApply = function() {
  var func = this;
  args = Array.prototype.slice.call(arguments);
  return function() {
    return func.apply(this, args.concat(
      Array.prototype.slice.call(arguments)
    ));
  };
};

如你所见,它的工作方式是对arguments这个特殊的值调用slice。

每一个函数又有一个特殊的内部变量叫做arguments,它是一个类似于数组的对象,包含传入函数的全部参数。
从技术层面说,它不是数组,因此它没有slice和forEach这些数组的方法。
这也就是为什么我们需要使用Array的slice.call方法。

现在我们通过一个例子看看如何使用它。这次我们不做数学题,来搞点有用的东西。
我们来建立一个把数字转换为16进制的小应用。

function nums2hex() {
  function componentToHex(component) {
    var hex = component.toString(16);
    // 确保返回的数值是两位数字,比如0c或12
    if (hex.length == 1) {
      return "0" + hex;
    } else {
      return hex;
    }
  }
  return Array.prototype.map.call(arguments,
    componentToHex).join(‘‘);
}
// 这个函数对多少个数字都有效
console.log(nums2hex()); // ‘‘
console.log(nums2hex(100, 200)); // ‘64c8‘
console.log(nums2hex(100, 200, 255, 0, 123)); // ‘64c8ff007b‘
// 不过我们可以用部分函数来对部分参数进行应用,比如mac地址的OUI
// ( OUI,“组织唯一标识符”,即网卡制造商的唯一标识符。)
var myOUI = 123;
var getMacAddress = nums2hex.partialApply(myOUI);
console.log(getMacAddress()); // ‘7b‘
console.log(getMacAddress(100, 200, 2, 123, 66, 0, 1));
// ‘7b64c8027b420001‘
// 我们还可以转换全红基础上的颜色rgb十六进制值
var shadesOfRed = nums2hex.partialApply(255);
console.log(shadesOfRed(123, 0)); // ‘ff7b00‘
console.log(shadesOfRed(100, 200)); // ‘ff64c8‘

这个例子展示出了我们可以应用部分参数而生成一个新的函数。它是左-右的,意思是我们只能部分应用从左边开始的若干参数。

右端部分应用

为了从右边开始应用参数,我们可以再定义一个补充函数。

Function.prototype.partialApplyRight = function() {
  var func = this;
  args = Array.prototype.slice.call(arguments);
  return function() {
    return func.apply(
      this, [].slice.call(arguments, 0)
      .concat(args));
  };
};

var shadesOfBlue = nums2hex.partialApplyRight(255);
console.log(shadesOfBlue(123, 0));   // ‘7b00ff‘
console.log(shadesOfBlue(100, 200)); // ‘64c8ff‘

var someShadesOfGreen = nums2hex.partialApplyRight(255, 0);
console.log(shadesOfGreen(123));   // ‘7bff00‘
console.log(shadesOfGreen(100));   // ‘64ff00‘

部分应用使我们能够创建非常一般化的函数,并从它提取出更多特殊化的函数。
但是这个方法最大的缺点在于参数传入的方式,也就是参数有多少个,是什么样的顺序,这些不太明确。
不明确性在编程中永远不是个好事儿。还有个更好的方式:珂理化。

珂理化(currying)

珂理化是这样一个过程:它把一个具有多个参数的函数转换为一个只有一个参数的函数并返回另一个函数,
这个被返回的函数需要原函数剩余的参数。正式的说法是:一个具有N个参数的函数可以被转换为具有N个函数的函数链,
其中每一个函数只有一个参数。

一个普遍的问题是:部分应用和珂理化有什么区别?实际就是部分应用立刻返回一个值,
而珂理化只返回另一个珂理化的函数来获取下一个参数,本质的区别是珂理化可以更好的控制参数传入的方式。
我们将会看到真的是这样,不过首先我们需要先创建一个呈现珂理化的函数。

这里我们再为Function的原型补充一个珂理化的方法。

Function.prototype.curry = function(numArgs) {
  var func = this;
  numArgs = numArgs || func.length; //func.length是调用此方法的函数的形参个数
  // 递归地获取参数
  function subCurry(prev) {
    return function(arg) {
      var args = prev.concat(arg);
      if (args.length < numArgs) {
        // 递归情形: 仍需要更多的参数
        return subCurry(args);
      } else {
        // 基准情形: 执行函数
        return func.apply(this, args);
      }
    };
  }
  return subCurry([]);
};
Function.prototype.curry = (numArgs) ->
  func = this
  numArgs or= func.length
  subCurry = (prev) ->
    (arg) ->
      args = prev.concat arg
      if args.length < numArgs
        subCurry args
      else
        func.apply this, args
  subCurry []

numArgs参数让我们可以在被珂理化的函数没有给出确切参数的时候指定参数的个数。

来看看用它来如何处理我们的十六进制应用。我们先写个函数,它会把RGB值转换为适合HTML的16进制字符串。

function rgb2hex(r, g, b) {
  // nums2hex is previously defined in this chapter
  return ‘#‘ + nums2hex(r) + nums2hex(g) + nums2hex(b);
}
var hexColors = rgb2hex.curry();
console.log(hexColors(11)) // 返回一个珂理化的函数
console.log(hexColors(11, 12, 123)) // 返回一个珂理化的函数
console.log(hexColors(11)(12)(123)) // 返回 #0b0c7b
console.log(hexColors(210)(12)(0)) // 返回 #d20c00

注意,curry方法返回的函数只接受一个参数,所以上例倒数第三行传入的三个参数的后两个是没用的。

这样使用珂理化不错。但是如果我们相对nums2hex()这个函数进行珂理化就会有点问题,因为这个函数没有指定参数,
你可以传入任意数量的参数。所以我们需要定义参数的个数。我们curry函数的那个可选的参数来设置被珂理化函数的参数个数。

var hexs = nums2hex.curry(2);
console.log(hexs(11)(12));     // 返回 0b0c
console.log(hexs(11));         // 返回一个函数
console.log(hexs(110)(12)(0)); // 不正确

所以珂理化不太适合可变参数的函数,对于这种情况,建议使用部分应用函数。

所有这些不只是利于函数工厂和代码重用,珂理化和部分应用在函数组合中扮演着更重要的角色。

下一节 函数组合

时间: 2024-08-01 18:20:48

JS函数式编程【译】4.1 部分函数应用和珂理化的相关文章

js 函数式编程 浅谈

js 函数式编程 函数式的思想, 就是不断地用已有函数, 来组合出新的函数. 函数式编程具有五个鲜明的特点: 1. 函数是"第一等公民" 指的是函数与其他数据类型一样,处于平等地位 2. 只用"表达式",不用"语句" "表达式"(expression)是一个单纯的运算过程,总是有返回值: "语句"(statement)是执行某种操作,没有返回值. 3. 没有"副作用" 指的是函数内部与外

js函数式编程(1)-纯函数

我将写的第一个主题是js的函数式编程,这一系列都是mostly adequate guide这本书的读书总结.原书在gitbook上,有中文版.由于原作者性格活泼,书中夹杂很多俚语,并且行文洒脱.中文译版难免有时需要思量一番,既然读了就写出来,能方便别人最好,也请读者指正.正文如下. 如果一个函数是纯函数,那么其不依赖外部环境,并且不产生副作用. 1.不依赖外部环境,反例如下: const a1 = 10; const aFunc1 = () => { // 依赖外部变量 return a1;

JS函数式编程【译】4.2 函数组合

?? Functional Programming in Javascript 主目录第四章 在Javascript中实现函数式编程的技术 函数组合 终于,我们到了函数组合. 在函数式编程中,我们希望一切都是函数,尤其希望是一元函数,如果可能的话.如果可以把所有的函数转换为一元函数, 将发生神奇的事情. 一元函数是只接受单个输入的函数.函数如果有多个输入就是多元的,不过我们一般把接受两个输入的叫二元函数, 把接受三个输入的叫三元函数. 有的函数接受的输入的数量并不确定,我们称它为可变的. 操作函

JS函数式编程【译】5.3 单子 (Monad)

单子是帮助你组合函数的工具. 像原始类型一样,单子是一种数据结构,它可以被当做装载让函子取东西的容器使用. 函子取出了数据,进行处理,然后放到一个新的单子中并将其返回. 我们将要关注三种单子: Maybes Promises Lenses 除了用于数组的map和函数的compose以外,我们还有三种函子(maybe.promise和lens). 这仅仅是另一些函子和单子. Maybe Maybe可以让我们优雅地使用有可能为空并且有默认值的数据.maybe是一个可以有值也可以没有值的变量,并且这对

JS函数式编程【译】4.在Javascript中实现函数式编程的技术

?? Functional Programming in Javascript 主目录上一章 建立函数式编程环境 第四章 在Javascript中实现函数式编程的技术 扶好你的帽子,我们现在要真正进入函数式的思想了. 这章我们继续下面的内容: 把所有的核心概念放到一个集中的范式里 探索函数式编程之美 一步步跟踪函数式模式相互交织的逻辑 我们将贯穿整章建立一个简单的应用做一些很酷的事情 你可能已经注意到,在上一章我们介绍Javascript的函数式库的时候引入了一些概念, 而不是在第二章<函数式编

JS函数式编程【译】5. 范畴论

?? Functional Programming in Javascript 主目录上一章 Javascript中实现函数式编程的技术 第五章 范畴论 托马斯·沃森(时任IBM董事长)说过一句著名的话,"我想全世界只有五台计算机的市场". 那是1948年,当时,每个人都认为计算机只会被用于两件事情:数学和工程. 即使是技术上最大胆的预想也不会认为有一天计算机能够把西班牙语翻译成英语, 或者模拟整个天气系统.在那时最快的计算机是IBM的SSEC,每秒能计算50次,显示终端要在15年后才

[原创译书] JS函数式编程 2.3 函数式程序员的工具集

?? Functional Programming in Javascript 主目录第二章 函数式编程基础上一节 与函数共舞 函数式程序员的工具集 如果你仔细看了到目前为止出现过的示例代码,你会发现这里面的一些方法不太熟悉. 它们是map().filter()和reduce()函数,它们对任何语言的函数式编程都至关重要. 它们可以让你不必使用循环和语句,写出更简洁的代码. map().filter()和reduce()函数组成了函数式程序员工具集的核心部分,这个工具集包括一系列纯的. 高阶的函

[原创译书] JS函数式编程 前言

前言 函数式编程是一种能够让你编写更聪明的代码的方式,可以减低复杂度,增强模块化. 它是一种通过灵巧地变化.组合.使用函数达到编写简洁代码的方式. Javascript提供了一个实现这些的超赞的途径.Javascript,这个Internet的脚本语言, 它的核心实际上是一个函数式语言.通过学习如何显露出它作为一个函数式语言的真实身份, 我们可以实现强大的.更易维护的以及更可靠的web应用. 通过这些,Javascript的那些怪癖和缺陷将会立刻变得清晰,并且语言本身也将会无限精彩. 学习如何使

[原创译书] JS函数式编程 2.1 函数式编程语言

?? Functional Programming in Javascript 主目录第二章 函数式编程基础 函数式编程语言 函数式编程语言是那些方便于使用函数式编程范式的语言.简单来说,如果具备函数式编程所需的特征, 它就可以被称为函数式语言.在多数情况下,编程的风格实际上决定了一个程序是否是函数式的. 是什么让一个语言具有函数式特征? 函数式编程无法用C语言来实现.函数式编程也无法用Java来实现(不包括那些通过大量变通手段实现的近似函数式编程). 这些语言不包含支持函数式编程的结构.他们是