一天入門Kotlin學習筆記(五)-常見高階函數

[toc]java

前言

這一節咱們主要說下Kotlin中關於數據集合中的經常使用高階函數git

map

map是遍歷一個數組遍歷的過程能夠對數組item進行操做(篩選、數據轉換等) ,返回一個新的數據集合
例子:github

val list = listOf(2, 8, 4, 5, 9, 7)
 //Kotlin 寫法 等價於 newList的轉化
 val newList1 = list.map {
        it * 3 + 2
    }
複製代碼
flatmap

就是把幾個小的list轉換到一個大的list中
例子:算法

val flatList = listOf(
        2..10,
        5..25,
        100..200
    )
    //flatten()  flatMap方法中無其餘操做能夠用flatten()
    val flatMapList = flatList.flatMap { intRange: IntRange ->
        intRange
    }
    
複製代碼

嵌套使用:編程

//上面flatMapList2表達式的完整寫法
    val flatMapList3 = flatList.flatMap(fun(intRange: IntRange): List<String> {
        return intRange.map(fun(intElement: Int): String {
            return "No.$intElement"
        })
    })
複製代碼
reduce

求list的和、求階乘
求和:數組

/reduce求list的和 acc是累加的結果 i是每次遍歷出來的元素
    val int: Int = list.reduce { acc, i -> acc + i }
複製代碼

求階乘:bash

//0->0 1->(1*1)*1 2->(1*1)*2 3->(1*2)*3
    (0..6).map(::factorial).forEach(::println)
    
    fun factorial(n: Int): Int {
    if (n == 0) return 1
    //至關於 n=3是 1*1,(1+2)*2,(1+2+3)*3,(1+2+3+4)*4
    return (1..n).reduce { acc, i -> acc * i }
    }
複製代碼
fold

是帶初始值的reduce 相對更強大,且對返回值無要求閉包

println((0..6).map(::factorial).fold(100) { acc, i -> acc + i })//100+873=973
複製代碼

字符串拼接: 這裏傳入的類型初始值是StringBuilder()app

println((0..6).map(::factorial).fold(StringBuilder())
    { acc, i -> acc.append(i).append(",") })
複製代碼
joinToString

字符串拼接函數式編程

println((0..6).joinToString("/", ".", ";"))
複製代碼
filter/takeWhile

根據條件篩選

println((0..6).map(::factorial))
    println((0..6).map(::factorial).filter { it % 2 == 1 })
    println((0..6).map(::factorial).takeWhile { it < 130 })//遇到第一個不知足條件的中止輸出
複製代碼
尾遞歸優化

Kotlin 支持一種稱爲尾遞歸的函數式編程⻛格。這容許一些一般用循環寫的算法改用遞歸函數來寫,而無堆棧溢出的⻛險。當一個函數用tailrec修飾符標記並知足所需的形式時,編譯器會優化該遞歸,留下一個快速而高效的基於循環的版本。

這是官網的說法。反正我是以爲有些晦澀。個人理解,首先理解什麼是尾遞歸。下看下下面的三個例子:

data class TreeNode(val value: Int) {
    var left: TreeNode? = null
    var right: TreeNode? = null
}


//尾遞歸
tailrec fun findListNode(head: ListNode?, value: Int): ListNode? {
    head ?: return null
    if (head.value == value) return head
    return findListNode(head.next, value)
}

//返回中存在 * 運算 因此是非尾遞歸
fun factorial(n: Long): Long {
    return n * factorial(n - 1)
}


//這個也是非 尾遞歸
fun findTreeNode(root: TreeNode?, value: Int): TreeNode? {
    root ?: return null
    if (root.value == value) return root
    return findTreeNode(root.left, value) ?: findTreeNode(root.right, value)

}

複製代碼

調用完本身以後沒有任何操做的遞歸就是尾遞歸尾遞歸優化就是在方法_上加tailrec關鍵地提示編譯器進行優化(將遞歸轉化味迭代進行處理)

若非尾遞歸加上tailrec也會提示(提示黃色警告)。

閉包

在函數爲一等公民的語言中,都具備閉包的特性。個人理解就是函數裏面聲明函數,函數裏面返回函數,這就是閉包。在Java中調用完方法,方法內部的狀態是不會被記住的,可是在Kotlin中,函數的狀態在調用後不會被銷燬。閉包有點像java的內部類,內部類持有外部類的引用,會致使外部類沒法釋放,也就是java中的內存泄漏。我我的覺的在Kotlin中閉包也會帶來消耗。

  1. 函數的運行環境
  2. 持有函數運行狀態
  3. 函數內部能夠定義函數
  4. 函數內部也能夠定義類
複合函數

自己不是語法上的關鍵字或是格式,是按照之前現有的知識,只不過在編寫上有點難以理解。這個只是函數的複合 沒有新的知識點
結合例子說明:

val add5 = { i: Int -> i + 5 }//g(x)

val multiplyBy2 = { i: Int -> i * 2 }//f(x)
fun main(args: Array<String>) {

    println(multiplyBy2(add5(8)))

    val add5AndMultiplyBy2 = add5 andThen multiplyBy2 //m(x)=f(g(x))  2*(8+5)=26
    println(add5AndMultiplyBy2(8))

    val add5AndMultiplyByCopy = multiplyBy2 andThen add5//m(x)=g(f(x))  2*8+5=21//先後參數類型相同能夠置換位置 不然是不能夠的 因此置換後的結果也是不一樣的
    println(add5AndMultiplyByCopy(8))

    val add5ComposeThen = add5 compose multiplyBy2
    println(add5ComposeThen(8))//m(x)=g(f(x)) 21

    val complexFunX = funFx complexFun funGxy
//    val complexFunXCopy =funGxy  complexFun funFx //這個就不能夠 類型參數是要根據條件
    println(complexFunX(3, 2))//3*3+50+2=61
}


//m(x)=f(g(x))   add5  andThen multiplyBy2至關於g(x).andThen(f(g(x)))=Function1<P1, P2>.andThen(f(g(x)))
//複合函數 擴展Function1的擴展方法 infix 中綴表達式
//Function1 傳入1個參數的函數 P1 接收的參數類型 P2返回的參數類型
//擴展方法andThen接收 一個參數的函數 他的參數 是add5的返回值 再返回最終結果
//andThen左邊的函數  Function1<P1,P2> 接收一個參數P1 返回結果P2
//andThen右邊的函數 function:Function1<P2,R> 參數爲左邊函數的返回值P2 返回結果R
//聚合的結果返回函數Function1<P1,R> 是以P1做爲參數 R作爲結果的函數
//至關於P1,P2 聚合 P2,R 返回 P1,R
//f(g(x))  P1至關於x P2 至關於g(x)返回值 返回的結果Function1<P1,R> R至關於f(g(x)) 的返回值
//Function1<P1,P2> 至關於g(x)
//function:Function1<P2,R> 至關於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))
    }
}

//compose左邊函數接收參數P2 返回R
//compse右邊函數 接收參數P1 返回P2
//返回結果函數P1,R
//至關於先執行右邊返回了P1,P2  在執行P2,R函數 聚合成P1,R
//g(f(x))
//f(x).compose(g(f(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))
    }
}

//課外擴展  m(x,y) = f(g(x,y)
val funFx = { i: Int -> i + 2 }
val funGxy = { i: Int, j: Int -> 3 * i + 100 / j }

//m(x,y) = f(g(x,y))
infix fun <P1, P2, P3, R> Function1<P3, R>.complexFun(function: Function2<P1, P2, P3>): Function2<P1, P2, R> {
    return fun(p1: P1, p2: P2): R {
        return this.invoke(function.invoke(p1, p2))
    }
}

複製代碼
柯里化函數(currying) -函數的鏈式調用
  • 柯里化函數就是把多個函數轉話成一個一個參數傳入
  • 柯里化就是將具備多個參數的函數,變成多個單個參數的函數,而後鏈式調用。注意調用時參數的順序不能顛倒

我的以爲 柯里化的意義在於:容許調用者分段調用。由於Kotlin是函數爲一等公民的語言。那麼假設有一個方法須要傳10個參數,可能A模塊傳了2個,而後返回函數,B模塊調用A模塊的方法並將其8個參數補齊,並真正使用。
例子:

//正常下的函數編寫:  
fun log1(tag: String, target: OutputStream, message: Any?) {
    target.write("[$tag] $message\n".toByteArray())
}

複製代碼

上面函數變化:

//這是另一種表達方式 與以前的函數表達結果相同
fun log2(tag: String) = fun(target: OutputStream) = fun(message: Any?) = target.write("[$tag] $message\n".toByteArray())
複製代碼

這就是柯里化函數。

再講將新的函數表達抽象就變成柯里化函數

//kotlin中柯里化鏈式調用的含義
fun <P1, P2, P3, R> Function3<P1, P2, P3, R>.curried() = fun(p1: P1) = fun(p2: P2) = fun(p3: P3) = this(p1, p2, p3)
複製代碼

調用:

// ::log1與 { tag: String, target: OutputStream, message: Any? -> log1(tag, target, message) } 是等價的 表示對函數的引用
//    { tag: String, target: OutputStream, message: Any? -> log1(tag, target, message) }.curried()("ggxiaozhi")(System.out)("Hello World!")
    log1("ggxiaozhi", System.out, "Hello World!")
    log2("ggxiaozhi")(System.out)("Hello World!!")
    //一個函數的參數複合柯里化版本 那麼就可使用::方法名字 如:::log1 拿到引用使用.curried()方法
    ::log1.curried()("ggxiaozhi")(System.out)("Hello World!!!")
複製代碼

這裏封裝成擴展方法,是爲了方便之後調用

偏函數

偏函數其實就是給多個參數的函數設置默認參數,那麼再使用的時候只須要傳入部分參數便可。

在上面柯里化函數的例子中,若是默認參數在前面,也可使用偏函數,如:

val consoleLogWithTag = (::log1.curried())("ggxiaozhi")(System.out)
    consoleLogWithTag("Hello World Tag")//偏函數
複製代碼

consoleLogWithTag方法就是一個偏函數。首先通過柯里化後,將第一個參數和第二個參數固定獲得consoleLogWithTag一個新的函數。那個這個函數其實就是偏函數

因此偏函數與柯里化函數存在必定的聯繫,當柯里化函數最前面的參數想設置默認值的時候可使用偏函數

下面咱們來看下真正的偏函數:

//partial2
    val bytes = "我是中國人".toByteArray(charset("GBK"))
    val stringFormGBK = makeStringFromGBKBytes(bytes)
    println(stringFormGBK)

    //partial1
    val stringFormGBKP1=makeStringFromGBKBytesp1(charset("GBK"))
    println(stringFormGBKP1)
    
    //偏函數  1-3
fun <P1, P2, R> Function2<P1, P2, R>.partial2() = fun(p2: P2) = fun(p1: P1) = this(p1, p2)//第一個參數默認 傳入第二個參數

fun <P1, P2, R> Function2<P1, P2, R>.partial1() = fun(p1: P1) = fun(p2: P2) = this(p1, p2)//第二個參數默認 傳入第一個
複製代碼

徹底可使用默認參數+具名參數的方式來實現參數的固定。若是須要固定的參數在中間,雖說能夠經過具名參數來解決,可是很尷尬,由於必須使用一大堆具名參數。由於默認參數你不傳就用默認參數,可是你傳入了,若是不使用具名參數那麼函數就會覺得你傳參數的位置是要覆蓋默認參數,因此必須具名函數所以偏函數就誕生了。偏函數就是一個多元函數傳入了部分參數以後的獲得的新的函數。

總結:

  1. 當柯里化後的函數 若是默認函數位置在參數的前面 那麼 能夠直接使用偏函數
  2. 若是函數的默認函數在氣其餘位置 那麼可使用擴展方法 FunctionN 來實現

結語

下篇咱們說下反射和泛型

Github源碼直接運行,包含所有詳細筆記

相關文章
相關標籤/搜索