Clojure Flavored Functional JavaScript
Clojure Flavored Functional JavaScript
Jichao Ouyang
Buy on Leanpub

Preface

这是一本你可能2小时就能看完的小书,但是涵盖了基本所有函数式编程的内容,还包含了一些 ECMAScript 6 定义的函数式新特性, 如箭头函数, 模式匹配等等. 还会介绍函数式一些重要概念在 JavaScript是如何实现即应用, 以及如何以函数式的思想编写 JavaScript 代码.

对果JavaScript的基本概念对你来说:

可能本书并不适合你, 请先移步 JavaScript Allonge, 但是如果你学习函数式编程时感到:

那么这本书将会对你会有所帮助.

我选用的 JavaScript 函数式库是 Eweda(Ramda 的最初实现,更遵守函数式教条,但由于 javascript 的栈很容易爆,Ramda的实现要更 pratical 一些而且可以用的产品代码中, 千万不要在产品中用 eweda,这里只用eweda做介绍目的)

为什么不用 Underscore 请移步第二章

由于会介绍 ECMAScript 6 的新特性, 书中很多写法都是 ECMAScript 6 标准, 只能在实现这些 feature 的浏览器(如 Firefox, 请目前参照各浏览器的实现情况) 里运行. 另外, 大多数的例子源码都会在文章里的 jsbin 链接里.

Lambda

为什么一开始就讲 lambda, 如果小时候玩过游戏”半条命”,那么你早都见过 lambda 了.

我从wikipedia里面粘出来了这么一段定义:

lambda包括一条变换规则(变量替换)和一条函数定义方式,Lambda演算之通用在于,任何一个可计算函数都能用这种形式来表达和求值。因而,它是等价于图灵机的.

好吧, 跟没解释一样. 简单来说lambda其实就是 xy 的映射关系, 但在大部分支持函数式的编程语言中, 它等价于匿名函数. 被称为 lambda 表达式. 因为这些函数只需要用一次, 而且变换不复杂, 完全不需要命名.

匿名函数在程序中的作用是可以作为参数传给高阶函数1, 或者作为闭包被返回.

但是匿名函数并不是原本的 lambda 算子, 因为匿名函数也可以接受多个参数, 如

1 multiple(x, y) = x*y

写成简单映射的形式, 把名字去掉

1 (x,y) -> x*y

这就是 lambda 了吗, 不是, lambda的用意是简化这个映射关系以至不需要名字, 更重要的是只映射一个 x.

什么意思呢? 让我们来分解一下上面的这个映射的过程.

  1. lambda 接受第一个参数 5, 返回另一个 lambda
    1   (5) -> (y -> 5*y) 
    
  2. 该返回的 lambda y -> 5*y 接收 y 并且返回 5*y, 若在用 4 调用该 lambda
1   4 -> 5*4

因此这里的匿名函数 (x,y)->x*y 看似一个 lambda, 其实是两个 lambda 的结合.

而这种接受一个参数返回另一个接收第二个参数的函数叫柯里化2.

这里我们先忍一忍, 来看下 JavaScript 中的 lambda 表达式.

箭头函数(arrow function)

来看看越来越函数式の JavaScript

新的草案ECMAScript 6 (虽然说是草案,但你可以看到 Firefox 其实已经实现大部分的 feature)里我们越来越近了, 借助一下transcompiler例如Babel 我们完全可以在项目中开始使用es6了3

看看里面有一行 arrow function,为什么叫箭头函数,还记得前面说lambda是提到的箭头吗。而且如果你之前用过 Haskell(单箭头) 或者Scala(双箭头), 会发现都用的是箭头来表示简单映射关系.

由于 arrow function 只在Firefox 22以上版本实现, 本节的所有代码都可以在Firefox的Console中调试, 其他chrome 什么的都没有实现(完全)4. 另外每节的最后我都会给出完整代码的可执行的 jsbin 链接.

声明一个箭头函数

你可以用两种方式定义一个箭头函数

1 ([param] [, param]) => {
2    statements
3 }
4 // or
5 param => expression

单个表达式可以写成一行, 而多行语句则需要 block {} 括起来.

为什么要用箭头函数

看看旧的匿名函数怎么写一个使数组中数字都乘2的函数.

1 var a = [1,2,3,4,5];
2 a.map(function(x){ return x*2 });

用箭头函数会变成

1 a.map(x => x*2);

只是少了 functionreturn 以及 block, 不是吗? 如果觉得差不多, 因为你看惯了 JavaScript 的匿名函数, 你的大脑编译器自动的忽略了,因为他们不需要显示的存在.

map(x => x*2) 要更 make sense, 因为我们需要的匿名函数只需要做一件事情, 我们需要的是 一个函数 f, 可以将给定 x, 映射到 y. 翻译这句话的最简单的方式不就是 f (x > x*2)

Lexical this

如果你觉得这还不足以说服改变匿名函数的写法, 那么想想以前写匿名函数中的经常需要 var self=this 的苦恼.

 1       var Multipler = function(inc){
 2         this.inc = inc;
 3       }
 4       Multipler.prototype.multiple = function(numbers){
 5         return numbers.map(function(number){
 6           return this.inc * number;
 7         })
 8       }
 9       new Multipler(2).multiple([1,2,3,4]) 
10   // => [NaN, NaN, NaN, NaN]  不 work, 因为 map 里面的 this 指向的是全局变量( window)
11 
12       Multipler.prototype.multiple = function(numbers){
13         var self = this; // 保持 Multipler 的 this 的缓存
14         return numbers.map(function(number){
15           return self.inc * number;
16         })
17       }
18       new Multipler(2).multiple([1,2,3,4]) // => [ 2, 4, 6, 8 ]

很怪不是吗, 确实是 Javascript 的一个 bug, 因此经常出现在各种面试题中.

试试替换成 arrow function

1   Multipler.prototype.multiple = function(numbers){
2     return numbers.map((number) => number*this.inc);
3   };
4 
5   console.log(new Multipler(2).multiple([1,2,3,4]));// => [ 2, 4, 6, 8 ]

不需要 var self=this 了是不是很开心☺️现在, arrow function 里面的 this 会自动 capture 外层函数的 this 值.

完整代码

JavaScript的匿名函数(anonymous function)

支持匿名函数, 也就意味着函数可以作为一等公民. 可以被当做参数, 也可以被当做返回值.因此, JavaScript 的支持一等函数的函数式语言, 而且定义一个匿名函数式如此简单.

创建一个匿名函数

在JavaScript里创建一个函数是如此的 ~~简单~~ … 比如:

1   function(x){
2       return x*x;
3   }// => SyntaxError: function statement requires a name

但是, 为什么报错了这里. 因为创建一个匿名函数需要用表达式(function expression). 表达式是会返回值的:

1   var a = new Array() // new Array 是表达式, 而这整行叫语句 statement

但为什么说 function statement requires a name. 因为 JavaScript 还有一种 创建函数的方法–/function statement/. 而在上面这种写法会被认为是一个 function 语句, 因为并没有期待值. 而 function 语句声明是需要名字的.

简单将这个函数赋给一个变量或当参数传都不会报错, 因为这时他没有歧义,只能是表达式.比如:

1   var squareA = function(x){
2       return x*x;
3   }

但是这里比较 tricky 的是这下 squareA 其实是一个具名函数了.

1 console.log(squareA) // => function squareA()

虽然结果是具名函数,但是过程却与下面这种声明的方式不一样.

1   function squareB(x){
2       return x*x;
3   } // => undefined

squareB 用的是 function statement 直接声明(显然 statement 没有返回), 而 squareA 则是先用 function expression 创建一个匿名函数, 然后将返回的函数赋给了名为 squareA 的变量. 因为表达式是有返回的:

1   console.log(function(x){ return x*x});
2   // => undefined
3   // => function ()

第一个 undefinedconsole.log 的返回值, 因此 function() 则是打印出来的 function 表达式创建的匿名函数.

使用匿名函数

JavaScript 的函数是一等函数. 这意味着我们的函数跟值的待遇是一样的,于是它

可以赋给变量:

1   var square = function(x) {return x*x}

可以当参数, 如刚才见到的:

1   console.log(function(x){return x*x})

将函数传给了 console.log

可以被返回:

1   function multiply(x){
2       return function(y){
3           return x*y;
4       }
5   }
6   multiply(1)(2) // => 2
  1. 第二章会详细解释高阶函数和闭包.
  2. 柯里化会在第二章详细讨论.
  3. 可以看看es6比较有意思的新特性 http://blog.oyanglul.us/javascript/essential-ecmascript6.html
  4. Chrome有一个 feature toggle 可以打开部分 es6 功能 <chrome://flags/#enable-javascript-harmony>

高阶函数(Higher-order function)

我们已经见识到了匿名函数和箭头函数的用法, 匿名的一等函数到底有什么用呢? 来看看高阶函数的应用.

高阶函数意思是它接收另一个函数作为参数. 为什么叫 高阶: 来看看这个函数 f(x, y) x(y)= 按照 lambda 的简化过程则是

1 f(x) => (y -> x(y))
2 (y) => x(y)

可以卡出来虽然只是调用 f, 但是返回还有一个函数x要求值.

还记得高等数学里面的导数吗, 两阶以上的导数叫高阶导数. 因为求导一次以后返回的可以求导.

概念是一样的, 如同俄罗斯套娃 当函数执行以后还需执行或者要对参数执行, 因此叫高阶函数.

高阶函数很多典型的应用如 map, reduce. 他们都是以传入不同的函数来以不同的方式操作数组元素.

而柯里化, 则是每次消费一个参数并返回一个逐步被配置好的函数.

高阶函数的这些应用都是为函数的组合提供灵活性. 在本章结束相信你会很好的体会到函数组合的强大之处.

Higher-order function

在 JavaScript 中, 使用高阶函数是非常方便的.

函数作为参数

假设我现在要对一个数组排序, 用我们熟悉的 sort

1   [1,3,2,5,4].sort( (x, y) => x - y )

如果我们要逆序的排序, 把减号左右的 xy 呼唤,就这么简单, 但如果我是一个对象数组, 要根据对象的 id 排序:

1   [{id:1, name:'one'},
2    {id:3, name:'three'}, 
3    {id:2, name:'two'}, 
4    {id:5, name:'five'},  
5    {id:4, name:'four'}].sort((x,y) => x.id - y.id)

是不是已经能够感受到高阶函数与匿名函数组合的灵活性.

函数作为返回值

函数的返回值可以不只是值, 同样也可以是一个函数, 来看 Eweda 内部的一个工具函数 aliasFor, 他的作用是给函数 E 的一些方法起一些别名:

Note 听起来很怪不是吗, 函数怎么有方法, 实际上 JavaScript 的 function 是一个特殊 对象, 试试在 Firefox console 里敲 console.log. 是不是看到了一些方法, 但是 typeof console.log 是 function

1   var E = () => {}
2   var aliasFor = oldName => {
3       var fn = newName => {
4         E[newName] = E[oldName];
5         return fn;
6       };
7       return (fn.is = fn.are = fn.and = fn);
8   };

这里有两个 return, 一个是 fn 返回自己, 另一个是 aliasFor 也返回 fn, 并且给 fn 了几个别名 fn.is fn.are

什么意思呢? fn 返回 fn. 很简单就是 fn() => fn, 那么 fn()()=>fn()=>fn …以此类推, 无论调用 fn 多少次,都最终返回 fn.

这到底有什么用呢, 由于这里使用了 fn 的副作用(side affect) 来干了一些事情 E[newName]=E[oldName], 也就是给 E 的方法起一个别名, 因此每次调用 fn 都会给 E 起一个别名. aliasFor 最后返回的是 fn 自己的一些别名, 使得可以 chain 起来更可读一些:

1   aliasFor('reduce').is('reduceLeft').is('foldl')

完整代码

另外, 函数作为返回值的重要应用, 柯里化与闭包, 将会在在后面专门介绍. 我们先来看下以函数作为参数的高阶函数的典型应用.

柯里化 currying

还记得 Haskell Curry吗

多巧啊, 人家姓 Curry 名 Haskell, 难怪 Haskell 语言会自动柯里化, 呵呵. 但是不奇怪吗, 为什么要柯里化呢. 为什么如此重要得让 Haskell 会默认自动柯里化所有函数, 不就是返回一个部分配置好的函数吗.

我们来看一个 Haskell 的代码.

1   max 3 4
2   (max 3) 4

结果都是4, 这有什么用呢.

这里看不出来, 放到 高阶函数 试试. 什么? 看不懂天书 Haskell, 来看看 JavaScript 吧.

我们来看一个问题

1. 写一个函数, 可以连接字符数组, 如 f(['1','2']) => '12'

好吧,如果不用柯里化, 怎么写? 啊哈 reduce

1 var concatArray = function(chars){
2   return chars.reduce(function(a, b){
3     return a.concat(b);
4   });
5 }
6 concat(['1','2','3']) // => '123'

很简单,对吧.

2. 现在我要其中所有数字加1, 然后在连接
1 var concatArray = function(chars, inc){
2   return chars.map(function(char){
3     return (+char)+inc + '';
4   }).reduce(function(a,b){
5       return a.concat(b)
6   });
7 }
8 console.log(concatArray(['1','2','3'], 1))// => '234'
3. 所有数字乘以2, 再重构试试看
 1 var multiple = function(a, b){
 2   return +a*b + ''
 3 }
 4 var concatArray = function(chars, inc){
 5   return chars.map(function(char){
 6     return multiple(char, inc);
 7   }).reduce(function(a,b){
 8       return a.concat(b)
 9   });
10 }
11 console.log(concatArray(['1','2','3'], 2)) // => '246'

是不是已经看出问题了呢? 如果我在需要每个数字都减2,是不是很麻烦呢.需要将 map 参数匿名函数中的 multiple 函数换掉. 这样一来 concatArray 就不能同时处理加, 乘和减? 那么怎么能把他提取出来呢? 来对比下柯里化的解法.

柯里化函数接口

 1 var multiple = function(a){
 2   return function(b){
 3     return +b*a + ''
 4   }
 5 }
 6 
 7 var plus = function(a){
 8   return function(b){
 9     return (+b)+a + ''
10   }
11 }
12 var concatArray = function(chars, stylishChar){
13   return chars.map(stylishChar)
14     .reduce(function(a,b){
15       return a.concat(b)
16   });
17 }
18 console.log(concatArray(['1','2','3'], multiple(2)))
19 console.log(concatArray(['1','2','3'], plus(2)))

有什么不一样呢 1. 处理数组中字符的函数被提取出来, 作为参数传入 2. 提取成柯里化的函数, 部分配置好后传入, 好处显而易见, 这下接口非常通畅 无论是外层调用

1 concatArray(['1','2','3'], multiple(2))

还是内部的 map 函数

1 chars.map(stylishChar)

这些接口都清晰了很多, 不是吗

这就是函数式的思想, 用已有的函数组合出新的函数, 而柯里化每消费一个参数, 都会返回一个新的部分配置的函数, 这为函数组合提供了更灵活的手段, 并且使得接口更为流畅.

自动柯里化

在 Haskell 语言中, 函数是会自动柯里化的:

1   max 3 4

其实就是

1 (max 3) 4

可以看看 maxmax 3 函数的 类型

1   ghci> :t max
2   max :: Ord a => a -> a -> a

看明白了么, Ord a => 表示类型约束为可以比较大小的类型, 因此=max= 的类型可以翻译成: 当给定一个=a=, 会得到=a -> a=, 再看看=max 3=的类型就好理解了

1 ghci> :t max 3
2 (Num a, Ord a) => a -> a

左侧表示类型约束 a 可以是 Ord 或者 Num, 意思是 max 3 还是一个函数,如果给定一个 Ord 或者 Num 类型的参数 则返回一个 Ord 或者 Num.

现在是不是清晰了, 在 Haskell 中每给定一个参数, 函数如果是多参数的, 该函数还会返回一个处理余下参数的函数. 这就是自动柯里化.

而在 Javascript(以及大多数语言) 中不是的, 如果给定多参函数的部分参数, 函数会默认其他参数是 undefined, 而不会返回处理剩余参数的函数.

1   function willNotCurry(a, b, c) {
2       console.log(a, b, c)
3       return a*b-c;
4   }
5   willNotCurry(1)
6   // => NaN
7   // => 1 undefined undefined

如果使用自动柯里化的库 eweda, 前面的例子简直就完美了

1 var multiple = curry(function(a, b){
2   return +b*a + ''
3 })
4 var plus = curry(function(a, b){
5   return (+b)+a + ''
6 })

完整代码

函数组合 function composition

通过前面介绍的高阶函数, map, fold 以及柯里化, 其实已经见识到什么是函数组合了. 如之前例子中的 map 就是 由 fold 函数与 reverse 函数组合出来的.

这就是函数式的思想, 不断地用已有函数, 来组合出新的函数.

如图就是函数组合,来自 Catgory Theory(Funtor 也是从这来的,后面会讲到), 既然从 A到B 有对应的映射f,B到 C有对应的映射g, 那么 (g.f)(x) 也就是 fg 的组合 g(f(x)) 就是 A到 C 的映射。上一章实现的 map 函数就相当于 reverse.fold.

pipe

类似 compose, eweda/ramda 还有一个方法叫 pipe, pipe 的函数执行方向刚好与 compose 相反. 比如 pipe(f, g), f 会先执行, 然后结果传给 g, 是不是让你想起了 bash 的 pipe

1 find / | grep porno

实际上就是 pipe(find, grep(porno))(/)

没错,他们都是一个意思. 而且这个函数执行的方向更适合人脑编译(可读)一些.

如果你已经习惯 underscore 的这种写法

1   _(data)
2     .chain()
3     .map(data1,fn1)
4     .filter(data2, fn2)
5     .value()

那么转换成 pipe 是很容易的一件事情,而且更简单明了易于重用和组合。

1   pipe(
2     map(fn1),
3     filter(fn2)
4   )(data)

完整代码