🦜

Kotlinにおける高階関数と関数リテラルについて説明する

2023/09/04に公開

TL;DR

Kotlin の世界における高階関数と関数リテラルについて説明します。

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

高階関数

パラメーターとして関数を取るか、関数を返す関数のこと。

プログラムングの概念として高階関数というものは存在するため、ここは他の言語と同じです。

パラメーターとして関数を取る関数
fun koukai(print: () -> Unit) {
    print()
}

fun print(){
    println("this is koukai")
}

fun main(){
    koukai(::print)
}
関数を返す関数
fun koukai(): () -> Unit {
    val print = fun() {
        println("this is koukai")
    }
    return print
}

fun main(){
    val func = koukai()
    func()
}

どちらも実行結果は同じです。

this is koukai

ラムダ式

  • 常に{}で囲まれている
  • パラメーターがあれば、 ->の前で宣言される
  • 本体が->の後に続く

Kotlin では関数の最後のパラメーターが関数である場合、そのパラメータはカッコの外に指定することができる。

fun koukai(message: String, print: (message: String) -> Unit){
    print(message)
}

fun main(){
    koukai("Hello World") { message -> println(message) }
}

ラムダが唯一の引数であれば、呼び出しの括弧を完全に省略できる

fun koukai(print: () -> Unit){
    print()
}

fun main(){
-    koukai() {
+    koukai {
        println("koukai")
    }
}

無名関数

無名関数とは以下のように、名前のない関数のことです。

- fun print(message: String): Unit { println(message) } //名前はprint
+ fun (message: String): Unit { println(message) } //名無し

ただし無名関数はそれ単体で宣言することはできません。変数に格納することで利用できます。

またよく似ていますがラムダ式とも同じように思えますが、決定的に違うポイントがあります。

「???」という感じですがこれは次章で説明します。

関数リテラル

関数リテラル = 関数を表現する式のこと。

以下の関数リテラルたちは、どちらも関数として実行されているわけではないですが、式として渡せる状態になっています。

ラムダ式
val print = { message: String -> println(message) }

またこのラムダ式は以下の無名関数と同値です。

無名関数
val print = fun (message: String): Unit { println(message) }

つまり、ラムダ式も無名関数もどちらも関数リテラルであり、式として扱えます。

同じ式であることには変わりませんが、ラムダ式と無名関数には違いがあります。

ラムダ式は戻り値を指定できない

そのため明示的に戻り値の型を指定するのであれば、無名関数として定義する必要があります。

val sum = fun(x: Int, y: Int): Int = x + y

ラムダ式は単一パラメーターのみ持つ場合、引数の名前は暗黙でitとなる

fun koukai(message: String, print: (message: String) -> Unit){
    print(message)
}

fun main(){
    koukai("Hello World") { println(it) }
}

結果は以下の通りです。

Hello World

it = Hello World として渡されていることがわかります。

クロージャ

クロージャ=ある関数から見て外側のスコープで宣言された変数のこと

ラムダ式や無名関数はクロージャにアクセスできます。

fun main(){
    val ints = listOf(1, -2, 3, -4, 5)
    var sum = 0
    ints.filter { it > 0 }.forEach {
        sum += it
    }
    print(sum)
}

レシーバ付き関数リテラル

レシーバを理解するために以下のコードを見ます。

fun main(){
    val sum = fun Int.(other: Int): Int = this + other
    val ans = 1.sum(2)
    println(ans)
}

これは拡張関数との組み合わせですが、Intクラスに対して他のIntオブジェクトを受け取って自身と足し合わせたものを返す関数リテラルを定義しています。

実行結果は以下のようになります。

3

ここでいうレシーバとはIntオブジェクトのことです。

レシーバ関数リテラル

レシーバ型を文脈から推測することができる場合、ラムダ式はレシーバ関数リテラルとして使用することができます。

class HTML {
    var innerHtml = "innerHtml"
    fun body() { println("<body>$innerHtml</body>") }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // レシーバオブジェクトを生成
    html.init()        // そのレシーバオブジェクトをラムダに渡す
    html.innerHtml = "innerHtml2"
    return html
}

fun main(){
    val html = html { // レシーバ付きラムダ(=HTML.() -> Unit)がここから始まる
        body()   // レシーバオブジェクトのメソッドを呼んでいる
    }
    html.body()
}

ここでは文脈からbody()というのはHTMLオブジェクトのメソッドだということが推測できる。

この場合、ラムダ式の中でbody()をインスタンス化していないにも関わらず使用できている。

つまりHTML.() -> Unit = { body() }であり、initに与えている{ body() }のことをレシーバ関数リテラルと呼んでいる。

おわりに

この辺の構文はライブラリのなかを見たりするとガンガン使われているので、「ちゃんと理解していないと FW やライブラリの裏側を読んでいけないな」と思い記事にまとめました。

レシーバ付き関数リテラルあたりの理解は少し怪しいので、よりわかりやすいサンプルコードや説明があればぜひコメントで教えてください 🙏

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。

よろしければ Conpass からメンバー登録よろしくお願いいたします。

https://blessingsoftware.connpass.com/

Discussion