高阶函数(二)
原文鏈接:https://github.com/enbandari/Kotlin-Tutorials
上周我已經給大家推送了一篇關于高階函數的文章,這一期,我們繼續探討一些相關的有意思的話題。
1. 復合函數
大家一定見過下面的數學題吧:
求 f(g(x)) 的值。
解:設 m(x) = f(g(x)) …
?
m 就是 f 和 g 的復合。
我們在 Kotlin 當中要如何對函數進行復合呢?
val add5 = { i: Int -> i + 5 } val multiplyBy2 = { i: Int -> i * 2 }我們定義了這么兩個函數,接著這么調用它:
println(multiplyBy2(add5(2))) // (2 + 5) * 2add5 相當于我們的 g(x),multiplyBy2 相當于 f(x),那么上面的式子就相當于 f(g(x))。下面我們提供一個簡單的方式來復合這兩個函數,得到 m(x) = f(g(x)):
// f andThen g -> g(f(x)) infix fun <P1, P2, R> Function1<P1, P2>.andThen(function: Function1<P2, R>): Function1<P1, R> {return fun(p1: P1): R{return function.invoke(this.invoke(p1))} }這里面有幾個知識點,我請大家一起復習一下。
第一個就是 infix,中綴表達式,有了這個關鍵字,我們的 add5 在調用 andThen 方法時,就不需要用 .andThen() 的形式了,而是像使用操作符一樣。
第二個是擴展方法,andThen 其實就是 Function1
val add5AndMultiplyBy2 = add5 andThen multiplyBy2 println(add5AndMultiplyBy2(2))這個例子的輸出結果其實與前面的相同, ( 2 + 5 ) * 2 = 14
通過 andThen,我們看到一個全新的函數 add5AndMultiplyBy2 被創造出來,它其實就是 add5 和 multiplyBy2 的復合。
當然,有時候我們其實還需要這樣的結果:
val multiplyBy2AndAdd5 = add5 compose multiplyBy2 println(multiplyBy2AndAdd5(2))這個相當于 2 * 2 + 5 = 9。 我們簡單看下 compose 的實現:
// f compose g -> f(g(x)) infix fun <P1, P2, R> Function1<P2, R>.compose(function: Function1<P1, P2>): Function1<P1, R> {return fun(p1: P1): R{return this.invoke(function.invoke(p1))} }compose 與 andThen 的結果是完全相反的,f andThen g -> g(f(x)),而 f compose g -> f(g(x))。
這就是函數復合,其實你在初中數學就學過這些東西了。
2. Currying
Curry 也有咖喱的意思,不過這一節可并不是充滿咖喱味的。在函數式編程當中,Currying 也經常被翻譯成“科里化”,我們從這個名字完全讀不出它究竟是要干啥。為什么?因為,Curry 是個人名——Haskell Curry。
回到我們的程序當中,我們首先必須要搞清楚什么是 Currying?Currying 其實就是由一個多參數的函數構造出一系列只有一個參數的函數的方法。這么說可能還是有些抽象,我們直接上例子:
f(x,y,z) = x * y - z
我們有一個三元函數,這個沒什么復雜的,你在高中數學當中見到過比這個恐怖的多的式子。接著我們給它做個變式:
f(x,y,z) = k_{yz}(x)
其中,(k_{yz}(x)) 是關于 (x) 的一個函數,(yz) 可當做常量看待。而一旦傳入 (x) 的值以后,例如 (k_{yz}(x_0)) ,那么此時又有變換:
f(x_0,y,z) = k_{yz}(x_0) = m_{z,x=x_0}(y)
類似的,我們還能最終變換成:
f(x_0,y_0,z) = m_{z,x=x_0}(y_0) = n_{x=x_0, y=y_0}(z)
這么一個數學概念,其實就是 Currying。那么它到底想說明怎樣一件事情呢?大家看,參數是一個一個傳進來的,這就好比我們完成一件事情,也是對其進行肢解,然后一步一步完成的,通過 Currying,我們可以對一個函數的調用細節進行仔細的考量,甚至像流水線一樣處理,以實現我們的目標。
用程序的語言描述,假設我們有一個函數:
fun hello(x: String, y: Int, z: Double): Boolean{... }它 Currying 的結果便是:
fun curriedHello(x: String): (y: Int) ->(z: Double)-> Boolean{... }下面我們給出一個 Kotlin 的例子:
fun log(tag: String, target: OutputStream, message: Any?){... }這是一個日志打印的函數,第一個參數 tag 是一個日志的標識,第二個參數是日志的內容,第三個參數是日志打印的目標,這個可以是控制臺,也可以是文件,由調用者指定。
顯然,我們通常調試時,輸出日志都是直接到控制臺的,于是我們定義一個新函數:
fun consoleLog(tag: String, message: Any?) = log(tag, System.out, message)由于我們可能針對某一個問題不斷地調試,這些日志的 tag 也是相同的,那么我們又會定義一個新函數:
val TAG = ... ... fun consoleLogWithTag(message: Any?) = log(TAG, System.out, message)這樣看上去似乎沒什么問題,不過你有可能會想,我不過是臨時打幾行日志,真的有必要定義這么多函數?調試一段代碼還好,調試的內容多了呢,而且他們的 tag 都還不一樣,難道我要定義 consoleLogWithTag2 、consoleLogWithTag3 … 么?
顯然,如果你運用 Currying,問題就簡單的多了,只不過是定義一個局部變量嘛:
val consoleLogWithTag = (::log.curried())(TAG)(System.out)... //打印日志 consoleLogWithTag("This may be an error to call here.")其中 log.curried() 這個方法的簽名如下:
fun <P1, P2, P3, R> Function3<P1, P2, P3, R>.curried(): (p1: P1)->(p2: P2)->(p3: P3)-> R{... }注意,由于 log 是函數名,因此我們在獲取其對應的函數引用時需要加 ::。
好,說到這里,你可能直接去試前面的代碼,然后垂頭喪氣的告訴我,說我這代碼是騙人的,根本不能跑。為啥呢?因為根本沒有 curried() 這個方法啊。
對啊,非常遺憾,截止到 1.1RC 版,我們也沒有看到這樣的 API 出現在標準庫當中,所以我們只好自己搞咯:
fun <P1, P2, P3, R> Function3<P1, P2, P3, R>.curried()= fun(p1: P1) = fun(p2: P2) = fun(p3: P3) = this(p1, p2, p3)當然,這只是 Function3
3. 偏函數
我們再來看一下上一節這個打日志的例子,對于有三個參數的 log 函數,我們在極大多數的使用場景下都對前兩個參數傳入了相同的值:
fun log(tag: String, target: OutputStream, message: Any?){... }...val consoleLogWithTag = (::log.curried())(TAG)(System.out)其實,對一個多參數的函數,通過指定其中的一部分參數后得到的仍然是一個函數,那么這個函數就是原函數的一個偏函數了。從這個意義上來講,consoleLogWithTag 也可以認為是 log 的一個偏函數。
顯然,偏函數與 Currying 有一些內在的聯系,如果我們需要構造的偏函數的參數恰好處于原函數參數的最前面,那么我們是可以使用 Currying 的方法獲得這一偏函數的;當然,如果我們希望得到任意位置的參數被指定后的偏函數,那么我們就有足夠的理由使用一些更好的方法。
例如:
val makeString = fun(byteArray: ByteArray, charset: Charset): String{return String(byteArray, charset) }...val makeStringFromGbkBytes = makeString.partial2(charset("GBK")) //實際當中這個字節流可以是文件流,也可以是網絡數據等等 val gbkByteArray = ... println(makeStringFromGbkBytes(gbkByteArray))對于第二個參數 Charset,我們在國內有不少公司仍在用 GBK 編碼,那么在開發的過程中,我們就沒有必要每次都指定 GBK 這個編碼選項了,下面這一句代碼返回了一個 makeString 的偏函數,這個函數第二個參數確定為 charset(“GBK”)。
val makeStringFromGbkBytes = makeString.partial2(charset("GBK"))接下來,同樣我們需要給出 partial2 的實現:
fun <P1, P2, R> Function2<P1, P2, R>.partial1(p1: P1) = fun(p2: P2) = this(p1, p2) fun <P1, P2, R> Function2<P1, P2, R>.partial2(p2: P2) = fun(p1: P1) = this(p1, p2)我們看到,我們為 Function2 實現了兩個擴展方法 partial1 和 partial2,這兩個方法分別用來生成兩個參數分別被指定后的偏函數。
目前 Kotlin 標準庫尚且沒有對此提供支持,如果需要得到 FunctionN (N > 1) 的偏函數,那么我們需要把他們對應的 partialN 依次實現。
需要注意的是,makeString 是一個函數引用,可以直接用于調用函數的方法,這與上一節當中的 ::log 本質上是一樣的,只是二者的定義方式不同,希望大家不要感到困惑。
// log 是函數名 fun log(tag: String, target: OutputStream, message: Any?){... } ... val consoleLogWithTag = (::log.curried())(TAG)(System.out)4. 小結
本文主要給大家介紹了如何基于 Kotlin 的現有標準庫來實現一些函數式編程的特性,其實這些特性已經在 Github 的 funKTionale 當中給出,本文的內容也更多的是在向它致敬。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
- 上一篇: 细说 Lambda 表达式
- 下一篇: Kotlin极简教程:第9章 轻量级线程