Kotlinの「高階関数」と「関数リテラル」を理解してみよう

今回の記事の対象者:

以下のKotlinのコードが何やってるか分からない人

fun twice(n: Int, f: (Int) -> Int): Int = f(f(n))

fun main(args: Array) {
    val got = twice(5){ it + 1 }
    println(got)
}

最初に見た時は理解に苦しみました。
が、ルールを理解した上で読んでみると以外とすんなり理解できます。

今回は初心者向けにこのコードを噛み砕いて説明していきます。

スポンサーリンク

環境情報

  • kotlin_version = ‘1.3.30’

まず初めに

高階関数とは

引数として関数を受け取ったり、戻り値として関数を返すような関数のこと。

関数リテラルとは

関数というデータを直接コード上で表現できる形。
つまり関数リテラルの記述を書くと、その記述は関数オブジェクトとして扱うことが出来る。

コードリーディング

まずmain関数から見ていきます。

fun main(args: Array) {
    val got = twice(5){ it + 1 }
    println(got)
}

ここでやっているのは、「printlnを使って直前で定義したgotを出力している」ということ。

では、そのgotに代入しているものを見て見る。

val got = twice(5){ it + 1 }

twiceは直前で定義した関数であることは分かっています。
しかしtwiceの後に記述が色々くっついています。これは何でしょう・・・?

では、一つ一つ分解して見てみます。すると下記の2つに分かれる。

(5) 
{ it + 1 }

twiceは引数を一つ取る関数なのか・・・?」
「その後のラムダ式は何だ・・・?」

と、パッと見だと理解出来ない。ということで先にtwice関数の方を見て見ましょう。

twice関数

こちらがtwice関数の定義。括弧が連発していて呪文のように思える。

fun twice(n: Int, f: (Int) -> Int): Int = f(f(n))

まずは受け取る引数の定義を落ち着いて見てみる。

fun twice(n: Int, f: (Int) -> Int)

どうやら受け取る引数はnfの2つということがわかる。

  • nはInt型
  • f(Int) -> Int

ん・・・?

(Int) -> Intってなんぞや・・・?Intに括弧が付いてるけど?

関数オブジェクトの型

(Int) -> Intという形はどういう意味か?
答えを言うと、「関数オブジェクトの型」というものを表している。
つまり、引数として関数のオブジェクトを渡す際に用いる書き方となる。

左辺: 渡す関数が受け取る引数の型
右辺: 渡す関数が返す戻り値の型

という感じの記述になる。

(Int) -> Intとあって一見分かりづらいものの、

  • (Int) が受け取る引数の型(つまりInt型を引数に取る)
  • Int が返す引数の型(つまり、Int型を返す)

ということで、

fun twice(n: Int, f: (Int) -> Int)

twice関数は「Int」「関数オブジェクト」の2つを引数に取る、ということが分かる。

twice関数の戻り値

fun twice(n: Int, f: (Int) -> Int): Int = f(f(n))

受け取る引数を理解したので、残りを見ていく。

: Int = f(f(n))

: Int でこのtwice関数の戻り値がIntである事を記述している。

次に、= の後に f(f(n))とあるが、このイコールは決して代入のイコールではない。

では何のイコールか?

「関数の括弧は単一式を返す場合は省略でき、その際に = で式を繋ぐことが出来る。」

つまり、省略しない場合は以下のような書き方が出来る。

fun twice(n: Int, f: (Int) -> Int): Int {
    return f(f(n))
}

最後に、f(f(n))と括弧だらけの記述があるが、落ち着いて見れば特に問題はない。
fIntを引数に取ってIntを返す関数であることが分かっているため、

fnというIntを渡して返ってきた結果のInt」を更にfに渡す」

というだけの話。

これでtwice関数が何をやっているかは理解出来ました。

関数呼出側

もう一度main関数に戻ります。

twice関数は、Int(Int) -> Int の2つの引数を取るという話でした。

fun main(args: Array) {
    val got = twice(5){ it + 1 }
    println(got)
}

ということは、下記の記述がtwice関数に2つの引数を渡しているということになります。

twice(5){ it + 1 }

(5)Intを渡しているのは分かります。
{ it + 1 }は 何でしょう? 引数から察するに関数オブジェクトということは推測できます。

これはちょっと分かりにくいKotlinの省略記法の一つです。

関数オブジェクト

関数の定義は基本的には以下のように名前を付けて行います。

fun hoge(num: Int): Int = num + 1

この関数はhoge(2)のように使えます。

もし、この関数を他の関数の引数として渡したい場合は以下のようにします。

val hogehoge = ::hoge // ::を関数の前に付けるとその関数の関数オブジェクトが取得できる。

これで関数オブジェクトを取得することが出来ます。

が、「そもそも関数を命名して定義していなくても」関数オブジェクトを作成出来ます。

以下のようにラムダ式(無名関数)で書くのが主流です。

{ num: Int -> num + 1 }

関数の中身は先程のhogeと同じ。引数numを取って +1した値を返しています。
関数hogeと違うのは fun hogeのような命名がない(無名)点です。

関数オブジェクトの省略記法

もう一度twice関数に渡す関数オブジェクトを見てみます。

twice(5){ it + 1 }

{ it + 1 }と、省略された形になっています。
本来は{ num: Int -> num + 1 }のような形のはずです。

何故こうなっているか。
それは「受け取る引数の型」「受け取る引数の変数」が省略されています。

まず、「受け取る引数の型」ですが、

  • twice関数定義側で引数の型を明示していることで、型推論が出来るため省略可能。

最後に、「受け取る引数の変数」

  • 関数の受け取る引数が1つの場合、変数までも省略可能。
  • その場合、受け取った引数は暗黙の変数 itを利用することで使用可能。

結果として { it + 1 }という超省略記法に辿り着くことになりました。

引数に関数オブジェクトを取る場合

そして最後の疑問。

twice(5){ it + 1 }
  • この書き方だと引数1個しか渡していないのでは?
  • 引数の後にラムダ式をくっ付けているけど、どういうこと?

これもあるルールによる省略記法でした。

「ある関数の最後の引数が関数オブジェクトの場合、引数リストの外に記述できる。」

つまりこう書いているものを

twice(5, { it + 1 })

このような記述にしてもOKということになります。

twice(5){ it + 1 }

キッチリルールを把握していないと分かりにくい記法ですが、こういうカラクリでした。

まとめ

最後に「高階関数」と「関数リテラル」について復習します。

高階関数とは

引数として関数を受け取ったり、戻り値として関数を返すような関数のこと。

つまり、このような関数の事です。

fun twice(n: Int, f: (Int) -> Int): Int = f(f(n))

今回は「引数として関数を受け取っている」関数の方になります。

関数リテラルとは

関数というデータを直接コード上で表現できる形。
つまり関数リテラルの記述を書くと、その記述は関数オブジェクトとして扱うことが出来る。

つまり今回で言うと、下記のような形です。
関数を表現でき、関数オブジェクトとして引数に渡す事が出来ました。

{ it + 1 }

また、今回は使用しませんでしたが無名関数という表現方法もあります。

fun(n) = n + 1

コードをもう一度見る

これらすべての説明を踏まえた上で、もう一度コード全体を読んでみます。

fun twice(n: Int, f: (Int) -> Int): Int = f(f(n))

fun main(args: Array) {
    val got = twice(5){ it + 1 }
    println(got)
}

どうでしょうか、すんなりと理解できたと思います。
ルールさえ分かっていれば呪文のようなコードも正しく理解できました。

参考

サンプルコードなどは以下を参考にさせて頂きました。