[Day 6] 匿名函數 (anonymous function) & lambda expression

匿名函數 (anonymous function)

為什麼這個會叫做匿名函數呢?很簡單,因為 沒有定義 function 的名稱

// Anonymous function, 沒有 function name
fun(x: Int, y: Int): Int = x + y

fun(x: Int, y: Int): Int {
    return x + y
}

lambda expression

lambda expression 也是一種匿名函數,形式上是這樣的,以上力為例子的話

val sum: (x: Int, y: Int) -> Int = { x, y -> 
        x + y 
}

匿名函數 v.s lambda expression

如果兩者放在一起比較的話, 會長這樣 會發現兩者差別在於 fun, 和 : 還有 -> https://ithelp.ithome.com.tw/upload/images/20200915/20129902oRkhummLJP.png

沒有參數的匿名函數和 lambda expression 例子

恩...好像剛舉的的例子都太多參數看起來有點霧煞煞的,來看一下沒有參數的例子

匿名函數

這裏用 fun 宣告了匿名函數後,指定給一個變數

  • 第一個例子,有宣告回傳類型,所以最後要有 return
  • 第二個例子,沒宣告回傳類型,隱式返回最後一句的結果
val yearFunction = fun(): String { // 匿名函數
    val year = 2020
    return "it's $year "
}

val yearFunction2 = fun() { // 匿名函數
    val year = 2020
    "it's $year "
}

lambda expression

這裡使用 lambda expression,最後也指定給一個變數

  • 第一個例子,有宣告回傳類型,lambda expression 會隱式返回(不用寫 return)最後一句的結果
  • 第二個例子,比較簡潔的寫法,隱式返回(不用寫 return)最後一句的結果

val yearFunction3: () -> String = { // lambda expression
    val year = 2020
    "it's $year "
}

val yearFunction4 = { // lambda expression
    val year = 2020
    "it's $year "
}

那這時候會好奇,如果匿名函數或 lambda 不指定給變數呢?

https://ithelp.ithome.com.tw/upload/images/20200915/20129902n1nBt9qYCc.png

會發現第三種 lambda 是不行的,lambda 一定要指定到一個變數上面

IIFE (Immediately-Invoked Function Expression),立即執行函數

上面第三個例子 lambda,如果在大括號之外又立刻使用了()

這樣會變成 IIFE (立即執行函數),可以立刻得到結果

		// 結果: it's 2020
    println(
            {
                val year = 2020
                "it's $year "
            }()
    )

通常函數我們都不會立刻執行它,而是獨立出來以方便之後呼叫,但也有的時候會使用 IIFE 這樣的方式來執行函數

lambda expression 例子

這裡繼續講 lambda expression 的例子

一個參數的例子

val yearFunParameter: (year: Int) -> String = { year ->
    println("yearFunParameter2:")
    "it's $year "
}
// 只有一個參數時,lambda 內會自動擁有 it 這個 local 變數
val yearFunParameter2: (year: Int) -> String = {
    "it's $it "
}

// 簡潔的寫法,隱式返回最後一句的結果
val yearFunParameter3 = { year: Int ->
    "it's $year "
}

兩個參數的例子

val multiParameterFun: (Long, String) -> String = { id, name ->
    println("multiParameterFun: $id, $name")
    "name is $name, id is $id"
}

// 簡潔的寫法,隱式返回最後一句的結果
val multiParameterFunSimple = { id: Long, name: String ->
    println("multiParameterFun: $id, $name")
    "name is $name, id is $id"
}

歸納成這樣子,我覺得主要是看等號在哪裡,所以很明顯的,用下方的方式比較省事

val multiParameterFun: (type, type) -> 回傳值 type = { name, name ->
	....
}

// 簡潔的寫法,隱式返回最後一句的結果
val multiParameterFunSimple = { name: type, name: type ->
	....
}

Functional Programming

讓我們再回來看一下什麼是 FP

在 wiki 百科中 FP 的定義如下

In functional programming,functions are treated as first-class citizens,meaning that they can be bound to names (including local identifiers),passed as arguments,and returned from other functions,just as any other data type can.

因為 Kotlin 是一個不折不扣的 FP,所以當然函數也可以當作參數傳遞,函數也可以被當作回傳值

Higher order functions

這裡再解釋一下名詞,higher order functions 是指這個函數

  • 參數可以接受函數傳入

或者

  • 回傳值是函數
  • 或以上兩者兼具

就是 higher order functions,其實我認為跟 FP 的定義一樣

function 的參數是 function (函數)

標題看起來很繞口, 其實就是把 function 傳入 function 的參數 這是第一個例子,宣告了一個 lambda expression 的 countFunction, 然後傳入 count 這個函數

val countFunction = { char: Char -> char == 'd' }
"I don't like it".count(countFunction) //1

count 是 kotlin.text 下 CharSequence 內建的函數,可以接受一個 predicate lambda,這裏要做的是計算此字串中包含 d 這個字母的數量

https://ithelp.ithome.com.tw/upload/images/20200915/201299021rk3VD2sjV.png

可以看到這段程式其實是執行了一個 for,其中 this 指的就是 CharSequence,也就是前面我們的字串 "I don't like it",其中 if (predicate(element)) 就會換成我們的 lambda pression,

/**
 * Returns the number of characters matching the given [predicate].
 */
public inline fun CharSequence.count(predicate: (Char) -> Boolean): Int {
    var count = 0
    for (element in this) if (predicate(element)) ++count
    return count
}

結果就會變成像是這樣

fun countImpl(): Int {
    var count = 0
    for (element in "I don't like it") {
        if (element == 'd') {
            ++count
        }
    }
    return count
}

以下的寫法都是可行的,但會發現在編輯器建議的寫法會是最後兩種

"I don't like it".count({ char -> char == 'd' }) // 建議 lambda 寫在外面
"I don't like it".count({ it == 'd' }) // 建議 lambda 寫在外面

// 如果 lambda parameter 在最後面,還能寫成這樣
"I don't like it".count() { it == 'd' } // 像是這樣
"I don't like it".count { it == 'd' } // 像是這樣,除了

https://ithelp.ithome.com.tw/upload/images/20200915/20129902i7K5ZjUYkw.png

從上面的例子來看最後兩種寫法會發現,lambda expression 寫在()的最外面!不大習慣這樣的寫法呢...所以再來多看一個例子

另一個傳入參數的例子

另一個傳入參數的例子,這裡自訂了一個函數 runLambda,最後一個參數是一個 lambda expression 的函數,showUserStatus 就是我們要傳入的參數

fun runLambda(userName: String, showUserStatus: (Int, String) -> String) {
    val id = 83666
    println(showUserStatus(id, userName))
}

// ex2: parameter is function
val showUserStatus = { id: Int, name: String ->
    println("result is userName: $name, his/her id is $id")
    "userName: $name, his/her id is $id"
}
runLambda("Tim", showUserStatus)

如果 lambda parameter 在最後面,呼叫 runLambda 還能寫成這樣

// 如果 lambda parameter 在最後面,能寫成這樣
runLambda("Tim") { id, name ->
    println("result is userName: $name, his/her id is $id")
    "userName: $name, his/her id is $id"
}

特別記憶一下

像之前看到的 count 這個例子, 要特別記一下這種只有一個參數的 function, 且此參數是傳入一個 lambda, 就可以把 lambda 直接寫在外面, 連括號都不用, 這在之後很多有用的 Kotlin 函數像是 apply, also 等等或 Kotlin DSL 都會使用到! 一定要記住!

"I don't like it".count { it == 'd' } 

以上就是今天的內容! 明天再繼續函數的部分! 謝謝大家我們明天見!

今日練習的程式在這: 請點我