标签:
转帖: http://jimliu.net/2015/10/21/real-functional-programming-in-javascript-1/
箭头函数
其他语言里面一般叫做lambda表达式,其实我个人当然是喜欢这个名字,但是因为ES6的语言规范里就把它管叫箭头函数,自然文中还是会尽量这么说。
箭头函数的基本定义方式是:
1
|
(参数列表) => {
|
当只有一个参数的时候,可以省略括号,写成
1
|
参数名 => {
|
当函数体是一个表达式(而不是段落)的时候,可以隐式return
,写成
1
|
参数名 => 返回表达式
|
由于我们的“真·函数式编程”是禁用过程式编程的,不存在段落,于是你可以见到下文中几乎所有的箭头函数都是最简单的形式,例如x => x * 2
。
箭头函数可以返回函数,并且在返回函数的时候,它也可以隐式return
,因此可以像haskell一样构建curry风格的函数,如
1
|
const add = x => y => x + y
|
用传统的风格来“翻译”上面的add
函数,就是
1
|
functionadd(x){
|
调用的时候自然也是使用curry风格的逐个传参add(5)(3)
,结果就是8。
解构是现代编程语言当中一个非常非常甜的语法糖,有时候我们为了实现多返回值,可能会返回一个数组,或者一个KV,这里以数组为例
1
|
const pair = a => b => [a, b]
|
我们可以用解构一次性将数组中的元素分别赋值到多个变量如
1
|
const [a, b] = pair(‘first‘)(‘second‘)// a是‘first‘,b是‘second‘
|
参数结构就是在定义函数参数的时候使用结构
1
|
const add = ([a, b]) => a + b
|
在add
函数里面,数组[5, 3]
可以被自动解构成a
和b
两个值。数组解构有一个高级的“剩余值”用法:
1
|
const [first, ...rest] = [1, 2, 3, 4, 5] // first是1,rest是[2, 3, 4, 5]
|
可以把“剩余值”解构到一个数组,这里叫rest
。
关于解构语法的更多趣闻,可以看看我之前的一篇博客。
OK,前戏就到这里,下面进入主题。
1. 实现循环
命令式编程当中,循环是最基本的控制流结构之一了,基本的for
循环大概是这样:
1
|
for (var i = 0; i < arr.length; i++) {
|
看见了var i
和i++
了吗?因为不让用变量,所以在“真·函数式编程”当中,这样做是行不通的。
函数式语言当中使用递归实现循环。首先拆解一下循环的要素:
1
|
for(初始化; 终止条件; 迭代操作) {
|
使用递归来实现的话,无外乎也就是把迭代终止换成递归终止。也就是说,只要有上面4个要素,就可以构造出for
循环。首先将问题简化,我们只想遍历一个数组,首先定义一个迭代函数loop
1
|
functionloop_on_array(arr, body, i) {
|
上面的函数有几个地方不满足“真·函数式编程”的需要:
function
定义:这个最简单,改成箭头函数就行了if/else
:这个可以简单的通过条件表达式来解决body(i)
和loop(arr, body, i + 1)
这两句代码使用了顺序执行为了解除顺序执行,我们可以使用像“回调函数”一样的思路来解决这个问题,也就是说让body
多接收一个参数next
,表示它执行完后的下一步操作,body
将会把自己的返回值以参数的形式传给next
1
|
const body = item => next =>
|
这样需要修改body
是不爽的,因此可以将其进行抽象,我们写一个two_steps
函数来组合两步操作
1
|
const two_steps = step1 => step2 => param =>
|
这样,上面的两行顺序执行代码就变成了
1
|
two_steps (body)(_ => loop_on_array(arr, body, i + 1))(arr[i])
|
注意中间那个参数它是一个函数,而并不是直接loop(arr, body, i + 1)
,它所接收的是body(arr[i])
的结果,但是它并不需要这个结果。函数式语言当中常常用_
来表示忽略不用的参数,我们的“真·函数式编程”也会保留这个习惯。
这样通过two_steps
来让两步操作能够顺序执行了,我们可以完成遍历数组的函数了
1
|
const loop_on_array = arr => body => i =>
|
调用的时候就是
1
|
loop_on_array ([1, 2, 3, 4, 5])(item => console.log(item))(0)
|
但是你会发现最后那个(0)
其实是很丑的对吧,毕竟它总是0,还不能省略,所以我们还是可以通过构造一个新的函数来抽取递归内容
1
|
const _loop = arr => body => i =>
|
在上面的遍历的代码里,我们用for
循环的套路来实现了对一个数组的遍历。这个思想其实还不算特别functional,要让它逼格更高,不妨从map
这个函数来考虑。
map
就是把一个数组arr
通过函数f
映射成另一个数组result
,在Haskell里面map
的经典定义方式是
1
|
map :: (a -> b) -> [a] -> [b]
|
简单的说就是:
map
返回的结果是空数组map f xs
,作为返回值(数组)的剩余部分直接将上面的代码“翻译”成JS的话,大概是这个样子
1
|
const map = f => arr =>
|
利用解构语法来简化的话大概是这个样子
1
|
const map= f => ([x, ...xs]) =>
|
至于map
的用法大家其实都是比较熟悉的了,这里就只做一个简单的例子
1
|
const double = arr =>
|
接下来需要实现一个sum
函数,对一个数组中的所有元素求和,有了map
的递归思想,很容易写出来sum
1
|
const sum = accumulator => ([x, ...xs]) =>
|
依然会发现那个(0)
传参是无比丑陋的,用一开始那个loop_on_array
相同的思想提取一个函数
1
|
const _sum = accumulator => ([x, ...xs]) =>
|
计划通。
比较map
和sum
可以发现事实上他们是非常相似的:
sum
中体现为一个用来不断求和的数值,在map
中体现为一个不断被扩充的数组也许你觉得上面的map
实现并不是这个模式的,事实上它是的,不放把map
按照这个模式重新实现一下
1
|
const _map = f => accumulator => ([x, ...xs]) =>
|
和sum
的模式惊人的一致对么?这就是所谓的foldr
,foldr
是一个对这种迭代模式的抽象,我们把它简单的描述成:
1
|
// foldr :: (a -> b -> b) -> b -> [a] -> b
|
其中f
是一个“fold函数”,接收两个参数,第一个参数是“当前值”,第二个参数是“累加器”,f
返回一个更新后的“累加器”。foldr
会在数组上迭代,不断调用f
以更新累加器,直到遇到空数组,迭代完成,则返回累加器的最后值。
下面我们用foldr
来分别实现map
和sum
1
|
const map = f => foldr (x => acc => [f(x), ...acc]) ([])
|
这时候你会发现foldr的定义其实就是JavaScript里自带的reduce
函数,没错这俩定义是一样的,通过foldr
或者说叫reduce
抽象,我们实现了对数组的“有状态遍历”,相比于上面的loop_on_array
则是“无状态遍历”,因为“累加器”作为状态,是在不断的被修改的(严格的说它不是被修改了,而是用一个新值取代了它)。
用foldr
实现的sum
非常形象,就像把摊成一列的扑克牌一张一张叠起来一样。
“有状态”当然可以实现“无状态”,不care状态不就行了吗,所以使用foldr
来实现loop_on_array
也是完全没问题的
1
|
const loop_on_array = f => foldr(x => _ => f(x))(undefined)
|
呃,等等,为什么输出顺序是反的?是54321呢?很明显foldr
中的r
就表示它是“右折叠”,从递归的角度很好理解,无外乎先进后出嘛。所以要实现“左折叠”自然也有foldl
函数(这里的左折叠右折叠表示折叠的起始方向,就跟东风北风一个道理):
1
|
// foldl :: (b -> a -> b) -> b -> [a] -> b
|
用它重新实现loop_on_array
,注意这次f
的参数顺序和foldr
是相反的,这次是accumulator
在前、x
在后,这样能更形象的表达“左折叠”的概念
1
|
const loop_on_array = f => foldl(_ => x => f(x))(undefined)
|
在第一个for
循环的例子中,我们使用了命令式编程的思路,通过构造“顺序执行”组合函数来让“循环体”和“下一次迭代”这两个操作能够顺序执行。
这个思路毫无疑问是行得通的,但是似乎又有点命令式编程思想根深蒂固的感觉,于是在后面的例子里面,通过map
、sum
抽象出foldr
和foldl
函数,实现了“有状态遍历”。
foldr/foldl
是对数组(列表)操作的一个高度抽象,它非常非常强大。
而在第一个例子实现for
循环的过程中,我们费了老鼻子劲才构造出的“顺序执行”难道就这么被抛弃了?其实并没有,因为foldr/foldl
抽象的是对列表的操作,而“顺序执行”则是更为普适的将两个操作的顺序进行安排的方式。
标签:
原文地址:http://www.cnblogs.com/abapscript/p/5057292.html