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

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

函数组合

终于,我们到了函数组合。

在函数式编程中,我们希望一切都是函数,尤其希望是一元函数,如果可能的话。如果可以把所有的函数转换为一元函数,
将发生神奇的事情。

一元函数是只接受单个输入的函数。函数如果有多个输入就是多元的,不过我们一般把接受两个输入的叫二元函数,
把接受三个输入的叫三元函数。 有的函数接受的输入的数量并不确定,我们称它为可变的。

操作函数及其可接受数量的输入可以极富表达力。在这一节,我们将探索如何把小的函数组合成新的函数:
小的单元逻辑组合成的整个程序比这些函数本身之和还要大。

组合

组合函数使我们能够从简单的、通用的函数建立复杂的函数。通过把函数作为其它函数的构建单元,
我们可以建立真正模块化的应用,使其具有很棒的可读性和可维护性。

在我们定义compose()这个补充函数之前,你可以先通过下面的例子看看它是怎么工作的:

var roundedSqrt = Math.round.compose(Math.sqrt)
console.log( roundedSqrt(5) ); // Returns: 2
var squaredDate =  roundedSqrt.compose(Date.parse)
console.log( squaredDate("January 1, 2014") ); // Returns: 1178370

在数学里,函数f和g的组合定义为f(g(x))。在Javascript里,可以写成这样:

var compose = function(f, g) {
  return function(x) {
?????    return f(g(x));
  };
};
compose = (f, g) -> (x) -> f g x

不过如果就写成这样的话,我们就失去了对this的跟踪。解决方法是使用call()和apply()。
与柯里化相比,compose()这个补充函数相当简单:

Function.prototype.compose = function(prevFunc) {
  var nextFunc = this;
  return function() {
    return nextFunc.call(this, prevFunc.apply(this, arguments));
  }
}

为了展示它怎么用,来建个完整的例子,如下:

function function1(a){return a + ‘ 1‘;}
function function2(b){return b + ‘ 2‘;}
function function3(c){return c + ‘ 3‘;}
var composition = function3.compose(function2).compose(function1);
console.log( composition(‘count‘) ); // returns ‘count 1 2 3‘

你是否注意到function1函数最先被应用?这很重要,函数是从右往左应用的。

原文是“Did you notice that the function3 parameter was applied first?”。
意思应该是function3参数最先被应用,这个应该是作者弄错了,显然是funtion1最先被应用,
返回了“count 1”,而且这样顺序也是从右往左的。

序列——反向组合

由于很多人喜欢从左往右读东西,让函数也从左往右应用可以更通顺些。我们把这叫做序列而不是组合。

为了让顺序相反,我们需要交换nextFunc和prevFunc参数。

Function.prototype.sequence = function(prevFunc) {
  var nextFunc = this;
  return function() {
    return prevFunc.call(this, nextFunc.apply(this, arguments));
  }
}

现在可以用更加自然的顺序调用这些函数

var sequences = function1.sequence(function2).sequence(function3);
console.log( sequences(‘count‘) ); // returns ‘count 1 2 3‘

组合 vs. 链

下面是五种实现floorSqrt()函数组合的方式。它们看起来差不多,但是需要仔细观察。

function floorSqrt1(num) {
  var sqrtNum = Math.sqrt(num);
  var floorSqrt = Math.floor(sqrtNum);
  var stringNum = String(floorSqrt);
  return stringNum;
}

function floorSqrt2(num) {
  return String(Math.floor(Math.sqrt(num)));
}

function floorSqrt3(num) {
  return [num].map(Math.sqrt).map(Math.floor).toString();
}

var floorSqrt4 = String.compose(Math.floor).compose(Math.sqrt);

var floorSqrt5 = Math.sqrt.sequence(Math.floor).sequence(String);

// 所有的函数都可以这样调用
floorSqrt < N > (17); // Returns: 4

这里有些关键的区别需要细看:

  • 第一种方法很明显冗长且低效。
  • 第二种方式是个不错的一行代码,但是这种方式只要有几个函数应用就会变得可读性很差。

    我们说代码越少越好其实没说到点上。代码在有效指令越简洁的时候可维护性越好。
    如果你减少了屏幕上的字符数量却没有改变有效指令的实现,这只能得到相反的效果——代码难以理解,
    并且真的更难维护了。比如,当我们使用嵌套在一起的三目运算符时,我们就把许多指令放到了一行里面。
    这种方式减少了屏幕上的代码总量,但是这并没有减少代码实际的具体步骤。所以其效果就是模糊不清难以理解。
    让代码易于维护的那种简洁是有效减少具体指令(比如使用更简单的算法靠更少和/或更简单的步骤完成同样的结果),
    或者只是简单地把代码替换为消息,比如调用一个具有良好文档的API的库。
  • 第三种方式是一个数组函数的链,尤其是map函数。它工作得很好,但并非数学正确的。
  • 第四个是我们compose()函数的实际应用。所有的方法被强制为一元的,
    鼓励使用更好、更简单、更小函数的纯函数只做一件事情,并且做得很好。
  • 最后一种实现使用compose()函数相反的顺序,同样有效。

使用组合来编程

组合最重要的一个方面是,除了应用的第一个函数以外,他们使用纯函数、只接受一个参数的一元函数效果最好。

执行的第一个函数的输出传递给了第二个函数。也就是函数必须接受前一个函数所传给它的东西。
类型签名对其有重要作用。

类型签名用于明确地声明函数接受的输入类型是什么以及输出类型是什么。它首先被Haskell使用,
实际上Haskell在函数定义时使用它们是为了编译器使用它们。但是,在Javascript里,我们只能把了性签名放在代码注释里。
它们看起来是这样:foo :: arg1 -> argN -> output
例如:

// getStringLength :: String -> Int
function getStringLength(s){return s.length};
// concatDates :: Date -> Date -> [Date]
function concatDates(d1,d2){return [d1, d2]};
// pureFunc :: (int -> Bool) -> [int] -> [int]
pureFunc(func, arr){return arr.filter(func)}

我插几句:本书完全借用了Haskell的类型签名形式。看到上面代码第二个函数签名你可能会困惑,
为什么两个Date类型的参数之间也是箭头?应该写成 Date, Date -> [Date] 吧?
我觉得,在JS里面的确适合写成后者。而Haskell之所以在参数之间都用箭头,是因为Haskell的函数是天生柯里化的。
从Haskell的函数调用形式就能看出来:比如要在Haskell里调用这个函数,就写成 concatDates d1 d2。
表面上是少了参数的括号和参数之间的逗号,深入想一下,如果要给它填上括号,你会怎么加?
concatDates(d1,d2)当然是最容易想到的,如果我写成concatDates(d1)(d2)呢?要是JS肯定就错了,
但是在Haskell里没错,因为Haskell是天然柯里化的,你传给他一个参数,它就返回接受剩余参数的函数。
本质上,Haskell的函数都是按照柯里化的方式调用的,也就是concatDates(d1)(d2)这样。
也是由于这个原因,coffeescript的函数调用尽管可以不写括号,看起来很像Haskell,
但仍然不能省略参数间的逗号,否则就成了函数组合调用了。
至于Haskell为什么天生支持柯里化?告诉你个秘密,Haskell是个人名,它的全名是Haskell Curry。
curry可以译成咖喱,在编程领域译成柯里。

为了能真正尝到组合的甜头,所有应用都需要一个由一元纯函数组成的强大的集合。它们是更大的函数的结构单元,
这些大的函数使应用非常模块化、可靠、易维护

来看个例子。首先,我们需要许多结构单元函数。它们中的一些需要依赖于其它函数:

// stringToArray :: String -> [Char]
function stringToArray(s) {
  return s.split(‘‘);
}
// arrayToString :: [Char] -> String
function arrayToString(a) {
  return a.join(‘‘);
}
// nextChar :: Char -> Char
function nextChar(c) {
  return String.fromCharCode(c.charCodeAt(0) + 1);
}
// previousChar :: Char -> Char
function previousChar(c) {
  return String.fromCharCode(c.charCodeAt(0) - 1);
}
// higherColorHex :: Char -> Char
function higherColorHex(c) {
  return c >= ‘f‘ ? ‘f‘ :
    c == ‘9‘ ? ‘a‘ :
    nextChar(c)
}
// lowerColorHex :: Char -> Char
function lowerColorHex(c) {
  return c <= ‘0‘ ? ‘0‘ :
    c == ‘a‘ ? ‘9‘ :
    previousChar(c);
}
// raiseColorHexes :: String -> String
function raiseColorHexes(arr) {
  return arr.map(higherColorHex);
}
// lowerColorHexes :: String -> String
function lowerColorHexes(arr) {
  return arr.map(lowerColorHex);
}

现在来把它们组合在一起

var lighterColor = arrayToString
  .compose(raiseColorHexes)
  .compose(stringToArray)
var darkerColor = arrayToString
  .compose(lowerColorHexes)
  .compose(stringToArray)

console.log(lighterColor(‘af0189‘)); // Returns: ‘bf129a‘
console.log(darkerColor(‘af0189‘)); // Returns: ‘9e0078‘

我们甚至可以混合使用compse()和curry()。实际上,它们一起工作得很好。我们来借助组合的例子来打造珂理化的例子。
首先我们需要一些前面的辅助函数。

// component2hex :: Ints -> Int
function componentToHex(c) {
  var hex = c.toString(16);
  return hex.length == 1 ? "0" + hex : hex;
}
// nums2hex :: Ints* -> Int
function nums2hex() {
  return Array.prototype.map.call(arguments,
    componentToHex).join(‘‘);
}

我怎么感觉这两个函数的类型签名不对劲呢?第一个应该是int -> string,第二个应该是ints* -> string

首先我们需要建立柯里化和部分应用的函数,然后把它们组合成其它组合函数。

var lighterColors = lighterColor
  .compose(nums2hex.curry());
var darkerRed = darkerColor
  .compose(nums2hex.partialApply(255));
var lighterRgb2hex = lighterColor
  .compose(nums2hex.partialApply());

console.log(lighterColors(123, 0, 22)); // Returns: 8cff11 [原书代码错误,实际返回是8c]
console.log(darkerRed(123, 0)); // Returns: ee6a00
console.log(lighterRgb2hex(123,200,100)); // Returns: 8cd975

勘误:我实在是揣测不出来上面的第一个函数作者是像表达一个什么意图。把nums2hex柯里化了,
但是nums2hex这个参数没有形参,柯里化时只会对一个参数处理,也就是接受一个(一次)参数就会返回结果。
所以传给它3个参数也只会对第一个有效。如果是用nums2hex.curry(3)呢,也不对,它跟lighterColor组合后,
接受一次参数是返回的是函数也就把函数传给了lighterColor,必然报错。所以我也只能纠正最后的返回值是8c,
但这个函数要真是这样明显没有意义。

我们完成了!这些函数易读且直观。我们被迫从只做一件事的小函数开始,然后就能够把函数放在一起形成更多功能。

我们来看最后一个例子。先有个函数根据一个可变的值来减淡RBG值,然后我们用组合根据它创建一个新函数。

// lighterColorNumSteps :: string -> num -> string
function lighterColorNumSteps(color, n) {
  for (var i = 0; i < n; i++) {
    color = lighterColor(color);
  }
  return color;
}
// 现在我们可以这样建立函数:
var lighterRedNumSteps =
  lighterColorNumSteps.curry().compose(reds)(0, 0);

// 然后这样使用:
console.log(lighterRedNumSteps(5)); // Return: ‘ff5555‘
console.log(lighterRedNumSteps(2)); // Return: ‘ff2222‘

用同样的方式,我们可以轻松地创建更多的函数来建立更淡或更深的蓝色、绿色、灰色、紫色等等你所想要的。
这是建立API的一个极好的方式。

我们仅仅接触了函数组合能做的事情的一个表面。组合所做的是让控制脱离Javascript。一般Javascript是从左到右求值,
但是现在解释器会说“OK,有人来管它了,我来处理别的东西。”现在compose()函数控制了求值顺序!

这就是Lazy.js和Bacon.js等是如何能够实现惰性求值和无限序列这些东西的。下面我们会看看这些库怎么用。

下一节 最函数式的编程

时间: 2024-10-03 03:07:08

JS函数式编程【译】4.2 函数组合的相关文章

[原创译书] JS函数式编程 2.2 与函数共舞

?? Functional Programming in Javascript 主目录第二章 函数式编程基础上一节 函数式编程语言 与函数共舞 有时,优雅的实现是一个函数.不是方法.不是类.不是框架.只是函数. - John Carmack,游戏<毁灭战士>首席程序员 函数式编程全都是关于如何把一个问题分解为一系列函数的.通常,函数会链在一起,互相嵌套, 来回传递,被视作头等公民.如果你使用过诸如jQuery或Node.js这样的框架,你应该用过一些这样的技术, 只不过你没有意识到. 我们从J

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

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

js 函数式编程 浅谈

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

函数式编程和高阶函数

函数式编程 函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用.而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的. 函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数! Python对函数式编程提供部分支持.由于Python允许使用变量,因此,Python不是纯函数式编程语言. 一

函数(作用域,匿名函数,函数式编程,高阶函数)

一.函数作用域 1.函数名表示的是内存地址 1 def test1(): 2 print('in the test1') 3 def test(): 4 print('in the test') 5 return test1 6 7 print(test()) 打印:in the test<function test1 at 0x000001E90E452EA0> 2.函数的作用域只跟函数声明时定义的作用域有关,跟函数的调用位置无任何关系 1 name = 'xiaopang' 2 def f

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.2 函子 (Functors)

函子(Functors) 态射是类型之间的映射:函子是范畴之间的映射.可以认为函子是这样一个函数,它从一个容器中取出值, 并将其加工,然后放到一个新的容器中.这个函数的第一个输入的参数是类型的态射,第二个输入的参数是容器. 函子的函数签名是这个样子 // myFunctor :: (a -> b) -> f a -> f b 意思是“给我一个传入a返回b的函数和一个包含a(一个或多个)的容器,我会返回一个包含b(一个或多个)的容器” 创建函子 要知道我们已经有了一个函子:map(),它攫

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

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