版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址 http://www.cnblogs.com/Colin-Cai/p/11329874.html 做者:窗户 QQ/微信:6679072 E-mail:6679072@qq.com
对于函数式编程来讲,map/reduce/filter这几个算子很是重要,其中有的语言不是reduce而是fold,但功能基本同样,不过reduce的迭代通常只有一个方向,fold可能会分两个方向,这是题外话。html
这篇文章就是来理解map的语义和实现,使用Scheme、Python、JS三种语言来解释一下这个概念。sql
map的语义编程
所谓算子,或者说高阶函数,是指输入或输出中带有函数的一种函数。通常状况下算子可能指输入中带有函数的状况,而对于输出中带有函数并带有输入参数信息的,咱们不少状况下习惯叫闭包。数组
map算子(高阶函数)是想同时处理n个长度相同的array或list等,它的输入参数中存在一个参数是函数。微信
如图以一个简单的例子来演示map的做用,4个参数,一个参数是一个带有三个参数的函数f,另外三个参数是长度同样的list a、b、c。全部list依次按位置给出一个值,做为f的参数,依次获得的值组成的list就是map的返回值。闭包
给个实际的例子:app
map带上的参数中,函数是f:x,y->x-y,也就是的获得两个参数的差,带上两个list,分别是[10,9,8]和[1,2,3],则依次将(10,1)、(9,2)、(8,3)传给f,获得9,7,5,从而map返回的值是[9,7,5]。框架
不少时候,map函数的处理是针对一个array/list的转换,从而看重面向对象编程的JS,其Array对象就有一个map方法。函数式编程
map的一种实现函数
理解了map函数的语义以后,咱们天然从过程式的思路明白了如何一个个的构造结果list的每一个元素。但既然是函数式编程,通常来讲,咱们须要的不是过程式的思路,而是函数式的思路,最基本的思路是要去构造递归。
所谓递归,说白了就是寻找函数总体与部分的类似性。
咱们仍是用刚才的例子,用函数f:x,y->x-y,两个list为[10,9,8]和[1,2,3],咱们构造结果第一个数,须要先从[10,9,8]取出第一个元素10,从[1,2,3]中取出第一个元素1,用f:x,y->x-y做用获得9,此处[10,9,8]和[1,2,3]还剩下[9,8]、[2,3]还没有处理。而[9,8]、[2,3]的处理依然是map作的事情。因而这里就构造了一个递归:
1.处理每一个list的第一个元素,获得结果list的第一个元素
2.map递归全部list的剩余部分,获得结果list的其余元素
3.拼接在一块儿,获得结果list
过程当中,须要两个动做,一个对全部list取第一个元素,另外一个是对全部list取剩余元素。单看这两个动做,共同点都是对全部list作的,不一样点在于对每一个list作的不一样,一个是提取第一个元素,一个是提取剩余元素,因而咱们这里就能够提取共性,也就是抽象。
咱们先来作这个抽象,咱们但愿这样用,(scan s f),带两个参数,一个是s是一个list,另外一个是f,结果是一个和s等长度的list,它的元素和s的元素一一对应,由函数f转换而来。
和以前的map相似,这个也同样能够分为三部分:
1.处理s的第一个元素,为(f (car s))
2.scan递归s的剩余部分,为(scan (cdr s) f)
3.把二者用cons拼接在一块儿,为(cons (f (car s)) (scan (cdr s) f))
其实,这里少了一个边界条件,就是还得考虑s为空列的时候,返回也是空列。
因而scan的实现应该是
(define (scan s f) (if (null? s) '() (cons (f (car s)) (scan (cdr s) f))))
同理,map也同样有边界条件,咱们要考虑map所跟的那一组list都为空列的状况,这种状况返回也是空列。
因而map的实现应该是
(define (map f . s) (if (null? (car s)) '()
(cons
;处理每一个list最开头的元素
(apply op (scan s car))
;递归处理剩余部分
(apply map2 op (scan s cdr)))))
apply是函数式编程支持语言里经常使用的功能,在于展开其最后一个为list的参数,好比apply(f, (1,2,3))也就是f(1,2,3)。
而后,咱们考虑Python的实现,由于序偶(pair)并不是是Python的底层,咱们须要用list拼接来实现,JS也同样。Python下用list的加号来实现拼接,为了简单起见,咱们并不用生成器实现。
咱们来模仿以前的Scheme,先实现scan函数。
scan = lambda s,f : [] if len(s)==0 else [f(s[0])] + ([] if len(s)==1 else scan(s[1:],f))
Python的apply在早期版本里曾经存在过,后来都用*来取代了apply。好比f(*(1,2,3))在Python里就等同于f(1,2,3)
抛开这个不一样,取代了以后,咱们实现map以下
map = lambda f,*s : [] if len(s[0])==0 else [f(*scan(s, lambda x:x[0]))] + map(f, *scan(lst, lambda x:x[1:]))
JS彷佛比Python更看重面向对象,它的Array拼接用的是Array的concat方法,同时,它并无Python那样的语法糖,不能像Python那样切片而只能用Array的slice方法,甚至于apply也是函数的方法的样子。另外,JS对可变参数的支持是使用arguments,须要转换成Array才能够切片。这些让我以为彷佛仍是Python用起来更加顺手,不过这些特性让人看起来更加像函数式编程。另外,JS有不少框架,不少时候编程甚至看起来脱离了原始的JS。
因此如下map的实现虽然本质上和以前是一回事情,但写法看上去差异比较大了。
function map()
{
var op = arguments[0];
var scan = (s,f) => s.length==0?[]:[f(s[0])].concat(scan(s.slice(1),f));
var s = [].slice.call(arguments).slice(1);//先取得全部的list
return s[0].length==0 ? [] : [op.apply(this,scan(s, x=>x[0]))].concat(map.apply(this,[op].concat(list_do(s, x=>x.slice(1)))));
}
柯里化
函数式编程里,有一个概念叫柯里化,它将一个多参数的函数变成嵌套着的每层只有一个参数的函数。
咱们以Python为例子,咱们先定义一个普通的函数add
def add(a,b,c):
return a+b+c
而后再定义另外一个看起来有些诡异的函数
def g(a):
def g2(b):
def g3(c):
return f(a,b,c)
return g3
return g2
这个函数g怎么用呢?
咱们测试发现,g(1)(2)(3)获得6,也就是add(1,2,3)的结果,而g(1)、g(1)(2)都是函数,这种层层闭包方式就是柯里化了。
在此,咱们但愿设计一个函数来实现柯里化,curry(n ,f),其中f为但愿柯里化的函数,而n为f的参数个数。
好比以前g则为curry(3, add)。
curry同样能够经过递归实现,好比以前g是curr(3, add),若是咱们构造一个函数
h = lambda a,b : lambda c : add(a, b, c)
那么 g = curry(2, h)
为了对于全部的curry均可以如此递归,要考虑以前讨论的不定参数,Python下也就是用*实现,而Scheme用apply,重写h函数以下:
h = lambda *s : lambda c : add(*(s+(c,)))
因而,获得curry的Python实现:
def curry(n, f):
return f if n==1 else curry(n-1, lambda *s : lambda c : f(*(s+(c,))))
从而,咱们对于以前的g(1)(2)(3)也就是curry(3,add)(1)(2)(3),
再者,curry函数自己同样能够柯里化,
因而,还能够写成
curry(2, curry)(3)(add)(1)(2)(3)
不断对curry柯里化,如下结果都是同样的,
curry(2, curry)(2)(curry)(3)(add)(1)(2)(3)
curry(2, curry)(2)(curry)(2)(curry)(3)(add)(1)(2)(3)
...
Scheme的版本也就很容易根据上述Python的实现来改写,
(define (curry n f) (if (= n 1) f (curry (- n 1) (lambda s (lambda (a) (apply f (append s (list a))))))))
JS的版本中,也须要用到函数的方法apply来实现不定参数,以及数组的concat方法来实现数组拼接。
function curry(n, f)
{
return n==1 ? f : curry(n-1, function () {return a => f.apply(this, [].slice.apply(arguments).concat([a]))});
}
基于柯里化的map实现
这里引入柯里化的缘由,天然也是为了实现map。
咱们这样去想,咱们先把map的参数f柯里化,而后依次一步步的每次传一个参数,巧妙的利用闭包传递信息,直到最终算出结果。
以前实现的scan对于每一个元都采用相同的函数处理,这里要有所区别,每一个数据都有本身独立的函数来处理,因此处理的函数也组成一个相同长的list。
与以前几乎相同,只是f成了一个list。
(define (scan s f) (if (null? s) '() (cons ((car f) (car s)) (scan (cdr s) (cdr f)))))
而对于(map op . s)的定义,咱们首先要把op柯里化了,(curry (length s) op),由于op会有(length s)个参数。
同时,最终的结果是(length (car s))个元素的list,因此是(length (car s))个值按s来迭代,因此迭代初始值是(make-list (length (car s)) (curry (length s) op))。
最后,咱们顺着s从左到右的方向按照scan迭代一圈便可,咱们用R6RS的fold-left来作这事。
(define (map op . s) (fold-left scan (make-list (length (car s)) (curry (length s) op)) s))
Python下,scan也很容易修改:
scan = lambda s,f : [] if len(s)==0 else [f[0](s[0])] + ([] if len(s)==1 else scan(s[1:],f[1:]))
Python下的reduce和Scheme的fold-left语义基本一致,再者Scheme下的make-list在Python下用个乘号就简单实现了。
map = lambda f,*s : reduce(scan, s, [curry(len(s), f)] * len(s[0]))
Python3下reduce在functools里,须要事先import
from functools import reduce
JS下的scan却是修改起来没有什么难度,JS下的reduce是Array的一个方法,make-list是用一个分配好长度的Array用fill方法实现,JS的确太面向对象了。
function map()
{
var scan = (f,s) => s.length==0 ? [] : [(f[0])(s[0])].concat(scan(f.slice(1),s.slice(1)));
return [].slice.call(arguments).slice(1).reduce(scan ,(new Array(arguments[1].length)).fill(mycurry(arguments.length-1, arguments[0])));
}
另外一种借助柯里化的实现
咱们能够考虑map的柯里化,若是咱们能够先获得map的柯里化,那么就很容易获得最终的结果。
说白了,也就是我但愿这样:
(define (map op . s)
(foldl (lambda (n r) (r n)) map-currying-op s)
)
(curry (+ 1 (length s)) map) 是对map的柯里化,map-currying-op也就是要实现((curry (+ 1 (length s)) map) op)
最开始的时候,是意识到构造这个柯里化与以前scan有必定的类似性,须要利用其数据的list造成闭包,从而抽象出curry-map这个高阶函数。再者闭包所封装的数据中不只仅有各层运算中的list,还须要带有计算层次的信息,由于最终的一次scan的结果获得的并非函数,而是map的结果了,将计算层次和list造成pair,计算层次每日后算一个list,则减1,直到变成1了,下一步获得的就再也不是闭包。
(define (map op . s)
(define scan
(lambda (s f)
(if (null? s)
'()
(cons ((car f) (car s)) (scan (cdr s) (cdr f))))))
(define curry-map
(lambda (x)
(if (= (car x) 1)
(lambda (s) (scan s (cdr x)))
(lambda (s)
(curry-map
(cons
(- (car x) 1)
(scan s (cdr x))))))))
(define map-currying-op
(curry-map
(cons
(length s)
(make-list (length (car s)) (curry (length s) op)))))
(fold-left (lambda (n r) (r n)) map-currying-op s)
)
上述实现就是经过map的柯里化来实现map,可能比较复杂而拗口,我在构造实现的时候也一度卡了壳,这个很正常,形式化的世界里的确有晦涩的时候。
另外,实际上这里curry-map并非对map的柯里化,只是这样写更加整齐一些,其实也能够改变一下,真正获得map的柯里化,这个只是一个小小的改动。
(define curry-map
(lambda (x)
(if (pair? x)
(if (= (car x) 1)
(lambda (s) (scan s (cdr x)))
(lambda (s)
(curry-map
(cons
(- (car x) 1)
(scan s (cdr x))))))
(curry-map
(cons
(length s)
(make-list (length (car s)) (curry (length s) x)))))))
(define map-currying-op
(curry-map op))
有兴趣的朋友能够分析一下这一节的全部代码,在此我并不给出Python和JS的实现,有兴趣的可在明白了以后能够本身来实现。
结束语
以上的实现能够帮助咱们你们去从所使用语言的内部去理解这些高阶函数。但实际上,这些做为该语言基本接口的map/reduce/filter等,通常是用实现这些语言的更低级语言来实现,如此实现有助于提高语言的效率。好比对于Lisp,咱们在学习Lisp的过程能中,可能会本身去实现各类最基本的函数,甚至包括cons/car/cdr,可是要认识到现实,在咱们本身去实现Lisp的解释器或者编译器的时候,仍是会为了加速,把这些接口放在语言级别实现里。