Kotlinに限ったことではありませんが、高階関数には2種類あります。1つは「関数を引数に取る」関数、もう1つが「関数を返す」関数です。
今回は「高階関数入門」と題し、
- 関数を抽象化するメリットとは?
- 高階関数の定義方法と使い方
といったテーマで、関数を引数に取る関数についてお話ししていきたいと思います。
後半はラムダ式や関数オブジェクトを使っていきます。関数型や無名関数、ラムダ式の基本については前回の記事をご覧ください。
関数を抽象化するメリット
簡単に言ってしまうと、高階関数は通常の関数をより抽象化したものです。まず「関数を抽象化する」ことの意義を考えてみましょう。
こんな関数はダサすぎる
例えば次のような処理を関数で表現するとします。
- “鮭弁当”を作る
- “唐揚げ弁当”を作る
この2つを何も考えず、そのまま関数にするとこんな感じになります。
//鮭弁当を作る関数
fun makeSalmonBento(): String = "鮭弁当"
//唐揚げ弁当を作る関数
fun makeFriedChickenBento(): String = "唐揚げ弁当"
println(makeSalmonBento()) //鮭弁当
println(makeFriedChickenBento()) //唐揚げ弁当
これで望んだ処理はできますが、はっきり言ってくそダサいですよね?
関数の形を固めすぎているために応用が効きません。そのため処理のバリエーションを増やそうとすると、「同じような処理をする関数」が増殖してしまう悪い例です。
例えるなら、弁当の種類ごとに発注する工場が違うようなもの。
普通はこんなことしません。似たような関数がいくつも存在してしまうというのは、非常にダサいわけです。
関数を抽象化する
そこで、もう少しこの関数を使い回せるように抽象化してみましょう。
//「何か」の弁当を作る関数
fun makeBento(origin: String): String = "${origin}弁当"
println(makeBento("鮭")) //鮭弁当
println(makeBento("唐揚げ")) //唐揚げ弁当
今回の関数は引数を取り、「引数を元に、何かの弁当を作る」関数です。
「何か」は関数定義の中ではString型としか決められていません。この部分を実際に決めるのは呼び出し時、つまり発注元です。
最初のコードと比べてみてどうでしょうか。もうなんとか弁当を発注する先は1つでOKです。スッキリしたと思いませんか?
今回の関数は引数によって材料を抽象化することで、何を使って処理するかを呼び出し時に決めることができるようになりました。これによって結果的に関数の汎用性が高まり、関数の無駄な増殖を防ぐことに繋がります。
- 関数は抽象化することで汎用性が高まる
- 通常の引数は「材料」を抽象化する手段である
- 抽象化した部分は実行時のコードによって決定する
高階関数の威力
それでは、さらにこの抽象化を推し進めてみましょう。
fun makeSome(origin: String, func: (String)-> String): String{
return func(origin)
}
これは何をする関数でしょうか。弁当を作る? どこにもそんな処理はありません。
これは
- String型と関数を引数に取り、
- 受け取ったString型を受け取った関数に渡し、
- その戻り値を返す
高階関数です。
1つ前の例では「何を」処理するかを抽象化しましたが、今回はそれに加えて「どうするか」まで抽象化してしまいました。
抽象化するとどうなるか。その部分は呼び出し時に決定することになります。実際に高階関数を呼び出してみましょう。
println(makeSome("鮭", {it + "弁当"})) //鮭弁当
今回makeSome関数に渡しているのは「材料」と「処理」の2つ。「処理」とは今回の例で例えると工場そのものであり、弁当を作るためのレシピです。
この関数はいわば「受け付け」の関数であり、作れるものはもう弁当に限りません。「刺し身盛り合わせ」だろうが「エビピラフ」であろうが、レシピはその時に決めればいいわけです。
これが「処理まで抽象化」してしまう高階関数の威力です。
高階関数の記述方法
関数を引数に取る高階関数がどんなものか分かったところで、ここからは高階関数の作り方、呼び出し方をいくつか見ていきます。どんどん使って慣れていきましょう。
引数を取らない関数を取る
//Intと関数を取り、割り算した結果をDoubleで返す
fun example1(num: Int, func: ()-> Int): Double{
val x = func().toDouble()
return num / x
}
「引数を取らない関数」を受け取る高階関数。記述方法は第二引数の()
内が空になっただけ。
//3 ÷ 16
println(example1(3, {4 * 4})) //0.1875
ラムダ式を()の外に出す
呼び出しは上記のように表記しても構いませんが、ラムダ式がその関数の最後の引数である場合、ラムダ式を呼び出しの()
の外に出すことが可能です。
この呼び出し方法は使う場面が多いので押さえておいてください。
//ラムダ式が最後の引数であれば、ラムダ式を丸ごと()の外に出すことができる
println(example1(3){4 * 4}) //0.1875
呼び出しから()を省略する
もう1つ頻繁に使われるのが、呼び出し時のコードから()
そのものを無くしてしまう形です。この記法は高階関数の引数が関数のみである場合に有効となります。
//高階関数の引数が関数1つのみ
fun example1(func: ()-> Int): Double {
val x = func().toDouble()
return x
}
//元々の形。()の中にラムダ式を表記
example1({5})
//ラムダ式が最後の引数なら、()の外に出せる
example1(){5}
//さらにその結果、()内が空であれば、()を省略可能
example1{5}
引数を2つ取る関数を取る
//第一引数のStringの最初の2文字を数値に変換し、それをラムダ式に渡した結果を付け加える
fun example2(str: String, f: (a: Int, b: Int)-> Int){
val c = f(str[0].toInt(), str[1].toInt())
println("${c}杯の$str")
}
「引数を2つ取る関数」を受け取る場合の記述方法。関数定義の記述は特に難しい点はありません。
example2("ice") { a: Int, b: Int -> a+b} //204杯のice
引数を2つ以上取るラムダ式は、呼び出し時にパラメータやシングルアロー->
の表記を省略できないので注意しましょう。
関数オブジェクトを渡す
ラムダ式ではなく、同じ関数型である「関数オブジェクト」を渡す方法もあります。関数オブジェクトの場合、まずは既存の関数などから関数オブジェクトを取得しましょう。
//Listの要素を大文字にする関数
fun changeCase(list: List<String>): List<String> {
val changedList = list.map {it.toUpperCase()}
return changedList
}
//関数オブジェクトを取得
val cCase = ::changeCase
val 変数名 = ::関数名
とすることで関数オブジェクトが取得できました。
次にこの関数を受け取る高階関数を定義します。
//StringをListに変換し、関数に渡す高階関数
fun makeStringList(str: String, predicate: (List<String>)-> List<String>): List<String> {
val sp = str.split(",", " ", ignoreCase = false)
return predicate(sp)
}
実際に使っていきます。関数オブジェクトを渡すには、変数名をそのまま記述すればOKです。
//main関数内
println(makeStringList("Hello,my name is Pouhon", cCase))
//[HELLO, MY, NAME, IS, POUHON]