map的實現和柯里化(Currying)

  版權申明:本文爲博主窗戶(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函數。

  scanlambda 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爲但願柯里化的函數,而nf的參數個數。

  好比以前g則爲curry(3, add)

 

  curry同樣能夠經過遞歸實現,好比以前gcurr(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-1lambda *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也很容易修改:

  scanlambda s,f : [] if  len(s)== else [f[0](s[0])] + ([] if  len(s)== 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的解釋器或者編譯器的時候,仍是會爲了加速,把這些接口放在語言級別實現裏。

相關文章
相關標籤/搜索