今回の記事の対象者:
以下の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: 1.3.30
まず初めに
高階関数とは
引数として関数を受け取ったり、戻り値として関数を返すような関数のこと。
関数リテラルとは
関数というデータを直接コード上で表現できる形。
つまり関数リテラルの記述を書くと、その記述は関数オブジェクトとして扱うことが出来る。
実際に Kotlin のコードを見てみる
まず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)
どうやら受け取る引数はn
とf
の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))
と括弧だらけの記述があるが、落ち着いて見れば特に問題はない。
f
がInt
を引数に取ってInt
を返す関数であることが分かっているため、
「f
にn
という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 }
キッチリルールを把握していないと分かりにくい記法ですが、こういうカラクリでした。
まとめ
最後に「高階関数」と「関数リテラル」について復習します。
Kotlin の高階関数とは
引数として関数を受け取ったり、戻り値として関数を返すような関数のこと。
つまり、このような関数の事です。
fun twice(n: Int, f: (Int) -> Int): Int = f(f(n))
今回は「引数として関数を受け取っている」関数の方になります。
Kotlin の関数リテラルとは
関数というデータを直接コード上で表現できる形。
つまり関数リテラルの記述を書くと、その記述は関数オブジェクトとして扱うことが出来る。
つまり今回で言うと、下記のような形です。
関数を表現でき、関数オブジェクトとして引数に渡す事が出来ました。
{ it + 1 }
また、今回は使用しませんでしたが無名関数という表現方法もあります。
fun(n) = n + 1
Kotlin のコードをもう一度見る
これらすべての説明を踏まえた上で、もう一度コード全体を読んでみます。
fun twice(n: Int, f: (Int) -> Int): Int = f(f(n)) fun main(args: Array) { val got = twice(5){ it + 1 } println(got) }
どうでしょうか、すんなりと理解できたと思います。
ルールさえ分かっていれば呪文のようなコードも正しく理解できました。
参考
サンプルコードなどは以下を参考にさせて頂きました。
コメント