本文簡單介紹了一下函數式編程的各類基本特性,但願可以對於準備使用函數式編程的人起到必定入門做用。html
函數式編程,一個一直以來都酷,很酷,很是酷的名詞。雖然誕生很早也炒了不少年可是一直都沒有形成很大的水花,不過近幾年來隨着多核,分佈式,大數據的發展,函數式編程已經普遍投入到了實戰中。 java
然而現實中仍是有很多人不太瞭解這種編程範式,以爲這僅僅是一個逼格較高的名詞。咱們這裏就來簡單介紹一下這個舉手投足都充滿酷勁的小東西。 git
本文以後的代碼主要以 Java 和 Scala 爲主,前者說明如何在非函數式語言中實現函數式風格,後者說明在函數式語言中是如何作的。代碼比較簡單,不管你是否懂這兩門語言,相信都能很容易看懂。此外因爲函數式編程這幾個詞太長了,如下都以 FP 進行簡寫。github
所謂的函數是一等公民指的是在 FP 中,函數能夠做爲直接做爲變量的值。web
例編程
Scala後端
val add = (x: Int, y: Int) => x + y add(1, 2)
以上咱們定義了一個負責將兩個整型相加的匿名函數並賦值給變量 add
,而且直接將這個變量當前函數進行調用,這在大部分面向對象的語言中你都是沒法直接這樣作的。多線程
Java閉包
interface Adder { int add(int x, int y); } Adder adder = (x, y) -> x + y; adder.add(1, 2);
因爲 Java 並非函數式語言,因此沒法直接將函數賦值給變量,所以以上例子中咱們使用 SAM 轉換來實現近似功能。app
閉包是一種帶有自由變量的代碼塊,其最根本的功能就是可以擴大局部變量的生命週期。閉包相信不少人都很熟悉,在 JavaScript 中閉包無處不在,是一種很好用可是一不注意就會掉坑裏的特性。
例
Scala
var factor = 10 factor = factor * 10 val multiplier = (x: Int) => x * factor
以上例子中函數體使用了兩個參數,其中 x
只是很普通的函數參數,而 factor
則是函數體外定義的一個局部變量,且該變量能夠任意進行修改,因此對 factor
的引用使該函數變成了一個閉包。
Java
int factor = 10; // factor = factor * 10; Multiplier multiplier = (x) -> x * factor;
在 Java 中匿名函數只能引用外部的 final
變量,Java 8 雖然能夠省略 final
關鍵字,可是實際仍是沒有任何變化,因此第二句語句必須註釋掉。這也就是說在 Java 中實際是沒法使用自由變量的,所以 Java 是否有真正的閉包一直都是一個爭論點,這裏就很少牽扯了。
通常而言成員變量在實例建立時就會被初始化,而惰性求值能夠將初始化的過程延遲到變量的第一次使用,對於成員變量的值須要通過大量計算的類來講能夠有效加快實例的建立過程。
例
Scala
lazy val lazyField = { var sum = 0 for (i <- 1 to 100) { sum += i } sum }
在 Scala 中是經過關鍵字 lazy
來聲明惰性求值的。在以上例子中定義了一個從 1 加到 100 的惰性變量,在第一次訪問該變量時這個計算過程纔會被執行。
Java
Supplier<Integer> lazyField = () -> { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } return sum; };
Java 雖然在語言層面沒有提供該功能,可是能夠經過 Java 8 提供的 Supplier
接口來實現一樣的功能。
遞歸你們都知道,就是函數本身調用本身。
例
定義一個遞歸函數
def addOne(i: Int) { if (i > 3) return println(s"before $i") addOne(i + 1) println(s"after $i") }
調用以上函數並傳入參數 3 會打印以下語句
before 1 before 2 before 3 after 3 after 2 after 1
這就是遞歸的基本形式。在每次遞歸調用時程序都必須保存當前的方法調用棧,即調用 addOne(2)
時程序必須記住以前是如何調用 addOne(1)
的,這樣它才能在執行完 addOne(2)
後返回到 addOne(1)
的下一條語句並打印 after 1
。所以在 Java 等語言中遞歸一來影響效率,二來消耗內存,調用次數過多時會引發方法棧溢出。
而尾遞歸指的就是隻在函數的最後一個語句調用遞歸。這樣的好處是可使用不少 FP 語言都支持的尾遞歸優化或者叫尾遞歸消除,即遞歸調用時直接將函數的調用者傳入到下一個遞歸函數中,並將當前函數彈出棧中,在最後一次遞歸調用完畢後直接返回傳入的調用者處而不是返回上一次遞歸的調用處。
用簡單的示意圖便是由原來的
line xxx, call addOne -> addOne(1) -> addOne(2) -> addOne(3) -> addOne(2) -> addOne(1) -> line xxx
優化爲
line xxx, call addOne -> addOne(1) -> addOne(2) -> addOne(3) -> line xxx
純函數並非 FP 的特性,而是 FP 中一些特性的集合。所謂的純函數簡單來說就是函數不能有反作用,保證引用透明。即函數自己不會修改參數的值也不會修改函數外的變量,不管執行多少次,一樣的輸入都會有一樣的輸出。
例
定義三個函數
def add(x: Int, y: Int) = x + y def clear(list: mutable.MutableList): Unit = { list.clear() } def random() = Random.nextInt()
以上代碼中定義了三個函數,其中 add()
符合純函數的定義;clear()
會清除傳入的 List
的全部元素,因此不是純函數;random()
沒法保證每次調用都產生一樣的輸入,因此也不是純函數。
高階函數指一個函數的參數是另外一個函數,或者一個函數的返回值是另外一個函數。
例
參數爲函數
def assert(predicate: () => Boolean) = if (!predicate()) throw new RuntimeException("assert failed") assert(() => 1 == 2)
以上函數 assert()
接收一個匿名函數 () => 1 == 2
做爲參數,本質上是應用了傳名調用的特性。
返回值爲函數
def create(): Int => Int = { val factor = 10 (x: Int) => x * factor }
集合操做能夠說是 FP 中最經常使用的一個特性,激進的 FP 擁護者甚至認爲應該使用 foreach
替代全部循環語句。這些集合操做本質上就是多個內置的高階函數。
例
Scala
val list = List(1, 2, 3) list.map(i => { println(s"before $i") i * 2 }).map(i => i + 1) .foreach(i => println(s"after $i"))
以上定義了一個包含三個整形的列表,依次對其中每一個元素乘以 2 後再加 1,最後進行打印操做。輸出結果以下:
before 1 before 2 before 3 after 3 after 5 after 7
能夠看到 FP 中的集合操做關注的是數據自己,至於如何遍歷數據這一行爲則是交給了語言內部機制來實現。相比較 for
循環來講這有兩個比較明顯的優勢:1. 必定程度上防止了原數據被修改,2. 不用關心遍歷的順序。這樣用戶能夠在必要時將操做放到多線程中而不用擔憂引發一些反作用,編譯器也能夠在編譯時自行對遍歷進行深度優化。
Java
List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.stream() .map(i -> { System.out.println("before " + i); return i * 2; }).map(i -> i + 1) .forEach(i -> System.out.println("after " + i));
輸出
before 1 after 3 before 2 after 5 before 3 after 7
能夠從以上輸出看到對於集合操做 Scala 和 Java 的實現徹底不同。
Scala 中一個操做中的全部數據完成處理後才流向下一個操做,能夠看作每一個操做都是一個關卡。而 Java 則是默認使用了惰性求值的方式,而且概念很是相似 Spark。其各類集合操做主要分爲兩種: transformation 和 action。transformation 即轉換操做,全部返回 Stream
對象的函數都是 transformation 操做,該操做不會當即執行,而是將執行步驟保存在 Stream
對象中。action 即執行操做,action 沒有返回值,調用後會當即執行以前 Stream
對象中保存的全部操做。 map()
這樣的就是 transformation 操做,forEach()
就是 action 操做。
柯里化指的是將一個接收多個參數的函數分解成多個接收單個參數的函數的一種技術。
好比說有這樣一個普通的函數
def minus(x: Int, y: Int) = x - y
柯理化後就變成如下形式,一個減法操做被分割爲兩部分
def minusCurrying(x: Int)(y: Int) = x - y
調用以上兩個函數
minus(5, 3) minusCurrying(5)(3)
函數的部分應用指的是向一個接收多個參數的函數傳入部分參數從而得到一個接收剩餘參數的新函數的技術。
好比說有這樣一個包含多個參數的函數
def show(prefix: String, msg: String, postfix: String) = prefix + msg + postfix
得到部分應用函數
val applyPrefix = show("(", _: String, _: String) println(applyPrefix("foo", ")")) // (foo) val applyPostfix = show(_: String, _: String, ")") println(applyPostfix("(", "bar")) // (bar)
以上 applyPrefix()
是應用了 show()
的第一個參數的新函數,applyPostfix()
是應用了 show()
的最後一個參數的新函數。
函數指對於全部給定類型的輸入,老是存在特定類型的輸出。
偏函數指對於某些給定類型的輸入,可能沒有對應的輸出,即偏函數沒法處理給定類型範圍內的全部值。
定義一個偏函數
val isEven: PartialFunction[Int, String] = { case x if x != 0 && x % 2 == 0 => x + " is even" }
以上 isEven()
只能處理偶數,對於奇數則沒法處理,因此是一個偏函數。
偏函數能夠用於責任鏈模式,每一個偏函數只處理部分類型的數據,其他類型的數據由下一個偏函數進行處理。
val isOdd: PartialFunction[Int, String] = { case x if x % 2 != 0 => x + " is odd" } val other: PartialFunction[Int, String] = { case _ => "else" } val partial = isEven orElse isOdd orElse other println(partial(3)) // 3 is odd println(partial(0)) // else
除了以上特性,函數式編程中還有 Monoid,SemiGroup 等比較難以理解的概念,本文暫時不牽扯那麼深,留待有興趣的人自行調查。最後我想說的是使用函數式編程的確很阪本,可是多瞭解一種編程範式對於從碼農進化爲碼農++仍是頗有幫助的。
若是你對以上代碼有興趣的話能夠直接訪問 https://github.com/SidneyXu/JGSK。
做者信息
做者來自力譜宿雲 LeapCloud 團隊_UX成員:Sidney Xu【原創】
首發地址:https://blog.maxleap.cn/archives/964
簡介:多年後端及移動端開發經驗,現任 力譜宿雲LeapCloud UX 團隊成員,主要從事於 Android 相關開發,目前對 Kotlin 和 Ruby 有濃厚興趣。
近期線下技術活動預告:
活動主題:【技術分享】Vert.x中國用戶組(上海地區)第一次技術沙龍
分享內容:
分享主題1:JVM上的高性能Reative工具Vert.x3介紹
分享主題1:Vert.x在maxleap的最佳實踐
分享主題1:Vert-web註解封裝
活動時間:2016/07/24(週日) 14:00 至 2016/07/24 17:00
活動場地:上海浦東新區金科路2889弄(近祖沖之路)長泰廣場C座12層
報名連接:http://www.hdb.com/party/ydtru-comm.html