函數式編程雜談

本文首發於 vivo互聯網技術 微信公衆號 
連接: https://mp.weixin.qq.com/s/gqw57pBYB4VRGKmNlkAODg
做者:張文博

比起命令式編程,函數式編程更增強調程序執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷演進,逐層推導出複雜的運算。本文經過函數式編程的一些趣味用法來闡述學習函數式編程的奇妙之處。html

1、編程範式綜述

編程是爲了解決問題,而解決問題能夠有多種視角和思路,其中普適且行之有效的模式被歸結爲「編程範式」。編程語言突飛猛進,從彙編、Pascal、C、C++、Ruby、Python、JS,etc...其背後的編程範式其實並無發生太多變化。拋開各語言繁紛複雜的表象去探究其背後抽象的編程範式能夠幫助咱們更好地使用computer進行compute。python

1.命令式

計算機本質上是執行一個個指令,所以編程人員只須要一步步寫下須要執行的指令,好比:先算什麼再算什麼,怎麼輸入怎麼計算怎麼輸出。因此編程語言大多都具有這四種類型的語句:git

  1. 運算語句將結果存入存儲器中以便往後使用;
  2. 循環語句使得一些語句能夠被反覆運行;
  3. 條件分支語句容許僅當某些條件成立時才運行某個指令集合;
  4. 以及存有爭議的相似goto這樣的無條件分支語句。

使得執行順序可以轉移到其餘指令之處。github

不管使用匯編、C、Java、JS 均可以寫出這樣的指令集合,其主要思想是關注計算機執行的步驟,即一步一步告訴計算機先作什麼再作什麼。因此命令式語言特別適合解決線性的計算場景,它強調自上而下的設計方式。這種方式很是相似咱們的工做、生活,由於咱們的平常活動都是循序漸進的順序進行的,甚至你能夠認爲是面向過程的。也比較貼合咱們的思惟方式,所以咱們寫出的絕大多數代碼都是這樣的。算法

2.聲明式

聲明式編程是以數據結構的形式來表達程序執行的邏輯,它的主要思想是告訴計算機應該作什麼,但不指定具體要怎麼作(固然在一些場景中,咱們也仍是要指定、探究其如何作)。SQL 語句就是最明顯的一種聲明式編程的例子,例如:「SELECT * FROM student WHERE age> 18」。由於咱們概括剝離了how,咱們就能夠專一於what,讓數據庫來幫咱們執行、優化how。數據庫

有時候對於某個業務邏輯目前沒有任何能夠概括提取的通用實現,咱們只能寫命令式編程代碼。當咱們寫成之後,若是進行思考概括抽象、進一步優化,就爲之後的聲明式作下鋪墊。編程

經過對比,命令式編程模擬電腦運算,是行動導向的,關鍵在於定義解法,即「怎麼作」,於是算法是顯性而目標是隱性的;聲明式編程模擬人腦思惟,是目標驅動的,關鍵在於描述問題,即「作什麼」,於是目標是顯性而算法是隱性的。json

3.函數式

函數式編程將計算機運算視爲函數運算,而且避免使用程序狀態以及易變對象。這裏的「函數」不是指計算機中的函數,而是指數學中的函數,即自變量的映射。也就是說一個函數的值僅決定於函數參數的值,不依賴其餘狀態。好比f(x),只要x不變,不論何時調用,調用幾回,值都是不變的。比起命令式編程,函數式編程更增強調程序執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷演進,逐層推導出複雜的運算,而不是設計一個複雜的執行過程。函數做爲一等公民,能夠出如今任何地方,好比你能夠把函數做爲參數傳遞給另外一個函數、還能夠將函數做爲返回值。數組

函數式編程的特色:promise

  1. 減小了可變量的聲明,程序更爲安全;
  2. 相比命令式編程,少了很是多的狀態變量的聲明與維護,自然適合高併發多線程並行計算等任務,我想這也是函數是編程近年又大熱的重要緣由之一;
  3. 代碼更爲簡潔,可是可讀性是高是低也依賴於不一樣場景、仁者見仁智者見智。

2、函數式編程的一些趣味用法

1.Closure(閉包)

public class OutClass {

  private void helloWorld() {
    System.out.println("Hello World!");
  }

  public InnerClass getInnerClass() {
    return new InnerClass();
  }

  public class InnerClass {
    public void hello() {
      helloWorld();
    }
  }

  /**
   * @param args
   */
  public static void main(String[] args) {
    // 在外部使用OutClass的private方法
    new OutClass().getInnerClass().hello();
  }
}

在Java中有不少方式實現上述目的,由於咱們的做用域和JS有着巨大差別。可是借鑑閉包的原理,咱們來看一個場景。假設接口A有一個方法m;接口B也有一個同名的方法m,兩個方法的簽名徹底同樣可是功能卻不同。類C想要同時實現接口A和接口B中的方法。由於兩個接口中的方法簽名徹底一致,因此C只能有一個m方法,這種狀況下應該怎麼實現需求呢?

public class C implements A {

  @Override
  public void m() {
    //...
  }

  private void o() {
    //...
  }

  public D getD() {
    return new D();
  }

  class D implements B {
    @Override
    public void m() {
      o();
    }
  }

  public static void main(String[] args) {
      C c = new C();
      c.m();
      c.getD().m();
  }
}

2.Currying(柯里化)

我對柯里化(Currying)的理解:柯里化函數能夠接收一些參數,接收了這些參數以後,該函數並非當即求值,而是繼續返回另外一個函數,剛纔傳入的參數在函數造成的閉包中被保存起來,待到函數真正須要求值的時候,以前傳入的全部參數都能用於求值。

下面先經過JS(我的感受經過JS比較好理解)對柯里化有一個直觀的認識。

var calculator = function(x, y, z){
    return(x + y)* z;
}

調用:calculator( 2, 7, 3);

柯里化寫法:

var calculator=function(x){
  return function(y){
    return function(z){
      return(x + y)* z;
    };
  };
};

調用:calculator(2)(7)(3);

經過對比,咱們發現柯里化的數學描述應該相似這樣,calculator(2, 7, 3) ---> calculator(2)(7)(3)。

如今咱們來回頭看看柯里化較爲學術的定義,是把接受多個參數的函數變換成接受一個單一參數的函數,而且返回接受餘下的參數的新函數,這個新函數最後還能返回全部輸入的運算結果。

Java 中的柯里化實現

Function<Integer, Function<Integer, Function<Integer, Integer>>> currying =
    new Function<Integer, Function<Integer, Function<Integer, Integer>>>() {

    @Override
    public Function<Integer, Function<Integer, Integer>> apply(Integer x) {
        return new Function<Integer, Function<Integer, Integer>>() {

            @Override
            public Function<Integer, Integer> apply(Integer y) {

                return new Function<Integer, Integer>() {
                    @Override
                    public Integer apply(Integer z) {
                        return (x + y) * z;
                    }
                };
            }
        };
    }
};

//在這裏,咱們能夠發現,雖然依次輸入二、7,可是咱們並不會計算結果,而是等到最後輸入結束時纔會返回值。
Function function1 = curryingFun().apply(2);//返回的是函數
Function function2 = curryingFun().apply(2).apply(7);//返回的是函數
Integer value = curryingFun().apply(2).apply(7).apply(3);//參數所有輸入,返回最後的值

柯里化的爭論

(1)支持的觀點

  • 延遲計算,只有在最後的輸入結束纔會進行計算;
  • 當你發現你要調用一個函數,而且調用參數都是同樣的狀況下,這個參數就能夠被柯里化,以便更好的完成任務;
  • 優雅的寫法,語義更有表達力;

(2)不過也有一些人持反對觀點,參數的不肯定性、排查錯誤困難。

3.Promise

Promise 是異步編程的一種解決方案,比傳統的諸如「回調函數、事件」解決方案,更合理和更強大。ES6已經普遍應用。我在這裏主要分析兩個最多見的用法。

  • then

Promise實例生成之後,能夠用then方法分別指定resolved狀態和rejected狀態的回調函數。then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。

promise.then(function(value) {
 // success
}, function(error) {
 // failure
}).then(...);
  • all

Promise.all方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.all([p1, p2, p3]);

上面代碼中,Promise.all方法接受一個數組做爲參數,p一、p二、p3都是 Promise 實例,p的狀態由p一、p二、p3決定,分紅兩種狀況。

  • 只有p一、p二、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。
  • 只要p一、p二、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

下面是一個具體的例子:

// 生成一個Promise對象的數組
const promises = [1,2,3.....].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
   // ...
});

Java的實現

Java中的使用方法目前確實不如js方便,能夠看看CompletableFuture,給咱們提供了一些方法。

4.Partial Function

其定義以下:當函數的參數個數太多,能夠建立一個新的函數,這個新函數能夠固定住原函數的部分參數,從而在調用時更簡單。下面是基於Python的實現。我的以爲,最大的便利就是避免咱們再去寫一些重載的方法。不過暫時沒有看到partial的Java版本。看到這裏,你們確定認爲「偏函數」這個翻譯實在是不許確,若是直譯過來叫「部分函數」好像也不怎麼清晰,咱們姑且仍是稱其爲Partial Function。

# !/usr/bin/python
# -*- coding: UTF-8 -*-
from functools import partial
def multiply(x, y):
  return x * y
print(multiply(3,4))# 輸出12

multiply4 = partial(multiply, y =4)# 不須要定義重載函數
print(multiply4(3))# 輸出12

5.map/reduce

Java如今對map、reduce也作了支持,特別是map已是你們平常編碼的利器,相信你們也都不陌生了。map(flatMap)按照規則轉換輸入內容,而reduce則是經過某個鏈接動做將全部元素彙總的操做。可是在這裏我仍是使用Python的例子來進行闡述,由於我以爲Python看起來更簡潔明瞭。

# !/usr/bin/python
# -*- coding: UTF-8 -*-
from functools import reduce

def addTen(x):
    return x + 10

def add(x, y):
    return x + y

r = map(addTen, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print r  #[11, 12, 13, 14, 15, 16, 17, 18, 19]
total = reduce(add, r)
print total #[11, 12, 13, 14, 15, 16, 17, 18, 19]加和等於135

6.divmod

divmod是Python的函數,我之因此專門來說述,是由於它所表明的思想確實新穎。函數會把除數和餘數運算結果結合起來返回,以下。不過Java確定不支持。

//把秒數轉換成時分秒結構顯示
def parseDuration( seconds ):
    m, s = divmod(int(seconds), 60)
    h, m = divmod(m, 60)
    return  ("%02d:%02d:%02d" % (h, m, s))

3、關於Scala

上述不少特性,Scala都提供了支持,它集成了面向對象編程和函數式編程的一些特性,感興趣的同窗能夠了解一下。以前看過介紹,Twitter對於Scala的應用比較多,推薦閱讀 Twitter Effective Scala 。

4、結語:咱們爲何要學習函數式編程

在不少時候,無能否認命令式編程很好用。當咱們寫業務邏輯時會書寫大量的命令式代碼,甚至在不少時候並無能夠概括抽離的實現。可是,若是咱們花時間去學習、發現能夠概括抽離的部分使其朝着聲明式邁進,結合函數式的思惟來思考,能爲咱們的編程帶來巨大的便捷。

經過其餘語言來舉一反三函數式編程的奇技淫巧,確實能帶給咱們新的視野。我相信隨着機器運算能力不斷提高、底層能力更加完善,咱們也須要跳出如何作的思惟限制,更多地站在更高的抽象層去思考作什麼,方能進入一個充滿想象、神奇的computable world。

更多內容敬請關注 vivo 互聯網技術 微信公衆號

注:轉載文章請先與微信號:labs2020 聯繫。

相關文章
相關標籤/搜索