9-2. オブジェクトとしての関数
概要
Kotlin では、数値や文字などのオブジェクトと同様に、関数を一つのオブジェクトとして扱うことができます。
これにより、ある変数を用意して、それに関数を参照するオブジェクトを代入するといったことも可能です。
また、この関数をオブジェクトとして扱うという考え方ができることによって、無名関数やラムダ式の理解につなげることができます。
第一級オブジェクト (First-class Object)
第一級オブジェクトとは、以下のような性質を持つオブジェクトです。
- 関数を変数に格納する
- 関数に渡す
- 関数から返す
など…
ウィキペディアでの記載は以下となっています。
第一級オブジェクト(ファーストクラスオブジェクト、first-class object)は、あるプログラミング言語において、たとえば生成、代入、演算、(引数・戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる対象のことである。ここで「オブジェクト」とは広く対象物・客体を意味し、必ずしもオブジェクト指向プログラミングにおけるオブジェクトを意味しない。第一級オブジェクトは「第一級データ型に属す」という。
この言葉は1960年代にクリストファー・ストレイチーによって「functions as first-class citizens」という文脈で初めて使われた。
言語によって異なるが、第一級オブジェクトは概ね次のような性質をもつ。
https://ja.wikipedia.org/wiki/第一級オブジェクト
- 無名のリテラルとして表現可能である。
- 変数に格納可能である。
- データ構造に格納可能である。
- それ自体が独自に存在できる(名前とは独立している)。
- 他のものとの等値性の比較が可能である。
- プロシージャや関数のパラメータとして渡すことができる。
- プロシージャや関数の戻り値として返すことができる。
- 実行時に構築可能である。
- 表示可能である。
- 読み込むことができる。
- 分散したプロセス間で転送することができる。
- 実行中のプロセスの外に保存することができる。
Kotlin では、関数を参照するオブジェクトは、第一級オブジェクトとなります。
関数を参照するオブジェクトが第一級オブジェクトであることから、関数を引数にとる関数、関数を返す関数 (これらは高階関数と呼ばれます) を作成することができます。
関数参照 および 関数を参照するオブジェクトの型
Kotlin では、関数参照演算子( :: ) を使って関数を参照するオブジェクトを取得することができます。
関数を指し示すオブジェクトを関数参照と呼びます。
関数参照を取得することで、関数を他の変数に代入したり、引数として渡したり、戻り値として返すことができます。
関数参照演算子( :: ) の使い方
:: 演算子は、関数名、プロパティ名、コンストラクタの前に付けて使用します。
例)
この例では、::add と記述することで、add 関数への参照を取得し、addReference 変数に代入しています。
addReference は関数型 (Int, Int) -> Int の変数となり、add 関数と同じように呼び出すことができます。
fun add(a: Int, b: Int): Int { return a + b } val addReference = ::add // add 関数への参照を取得 val result = addReference(3, 5) // result には 8 が代入される
上記の add の関数は、返り値が1つだけなので、単一式関数での記載も可能です。
fun add(a: Int, b: Int) : Int = a + b // 単一式関数で定義 val addReference = ::add // add 関数への参照を取得 val result = addReference(3, 5) // result には 8 が代入される
関数型 (Function Type)、関数を参照するオブジェクトの型
関数を参照するオブジェクトの型は、関数型 (Function Type) と呼ばれます。
上記の例では、addReference に代入する際に、add の型を明記せずに型推論に任せた形となっていました。
明示的に型も指定した場合は以下のようになります。
すなわち、この場合の型は (Int, Int) -> Int となります。Int の引数を2つ取り、1つの Int 型 の値を返す関数型を示します。
fun add(a: Int, b: Int): Int { return a + b } val addReference : (Int, Int) -> Int = ::add // addReference の変数の型を明記、add への関数参照を代入 val result = addReference(3, 5) // result には 8 が代入される
一般的には、関数型は 以下ように パラメータの型をかっこで囲みアロー演算子で返り値の型を続ける形 で記載します。
(1番目の引数の型, 2番目の引数の型, ...) -> 返り値の型
無名関数 (Anonymous Function)
無名関数とは、名前を持たない関数のことです。
無名関数は、関数を変数に代入したり、引数として渡したりする際に便利です。
関数の具体的な内容(変数に代入できる値) を示す関数リテラル、すなわち、関数型の変数に代入する際の具体的な関数を表現した部分です。
例)
fun main() { val addfunc : (Int, Int) -> Int = fun (x: Int, y:Int) : Int = x + y println(addfunc(1,2)) }
この = の右辺 “fun (x: Int, y:Int) : Int = x + y" の部分が 無名関数 です。
関数定義部分は、ブロックによる記載方法 (Block Body) でも、式による記載方法(Expression Body) でも可能です。型推論により型の定義も省略することも可能です。
以下は addfunc1 ~ addfunc6 に代入されている関数は同じ動作となります。
val addfunc1: (Int, Int) -> Int = fun(x: Int, y: Int): Int { return x + y } val addfunc2: (Int, Int) -> Int = fun(x: Int, y: Int): Int = x + y val addfunc3: (Int, Int) -> Int = fun(x: Int, y: Int) = x + y val addfunc4 = fun (x : Int, y : Int) : Int { return x + y } val addfunc5 = fun(x: Int, y: Int): Int = x + y val addfunc6 = fun(x: Int, y: Int) = x + y
ラムダ式 (Lambda Expression)
ラムダ式 (Lambda expression) は、無明関数をより簡潔に記述する方法です。
ラムダ式は、{ } (波括弧) で囲み、パラメータと式をアロー演算子でつなぎます。
例)
val addfunc_lambda1 : (Int, Int) -> Int = { x: Int, y: Int -> x + y } // この右辺 { x: Int, y: Int -> x + y } がラムダ式
型推論が使えるので、関数型の変数宣言時の型宣言は省略可能です。
以下では、addfunc_lambda2 という変数を宣言していますが、この変数の型を宣言せず省略しています。addfunc_lambda1 と比較するとより簡潔な表現にできることがわかります。
val addfunc_lambda2 = { x: Int, y: Int -> x + y }
あるいは、関数側を明記しておけば、関数リテラル側のパラメータの型((上の例でいえば右辺の x と y の型) を省略することも可能です。
以下では、{ } (波括弧) 内での変数 x と y の型を宣言せず省略しています。
val addfunc_lambda3 : (Int, Int) -> Int = { x, y -> x + y }
また、ラムダ式においては、パラメータが一つの場合に限り、暗黙の変数 it を使用できます。
it は、ラムダ式内で自動的に定義され、単一の引数を参照します。
以下は、同じ処理になりますが、後者は 暗黙の変数 it を使っています。これにより 、{ } (波括弧) 内での変数の宣言とアロー演算子が省略できます。
val addtwice_lambda : (Int) -> Int = { i : Int -> i + i } val addtwice_lambda2 : (Int) -> Int = { it + it } //暗黙の変数 it を使った場合
高階関数 (High Order Function)
高階関数は、関数を引数として受け取ったり、返り値として関数を返す関数です。
高階関数を使用することで、コードの再利用性が向上し、より抽象的で柔軟なプログラムを作成できます。また、関数リテラルやラムダ式を使うことで、コードが簡潔になり、読みやすくなります。
例)
四則演算器 (加算器、減算器、乗算器、除算器) を生成する関数として fourArithmeticCalculator という高階関数を定義しています。
どういった演算を行いたいかを引数で指定し、それを行う関数オブジェクトを返します。
fourArithmeticCalculator(“adder") とした場合は、加算器の関数を返します。
fun fourArithmeticCalculatorCreator(operator: String): (Int, Int) -> Int { val arithmeticCalculatorFunction: (Int, Int) -> Int = when (operator) { "+" -> fun(x: Int, y: Int) = x + y "-" -> fun(x: Int, y: Int) = x - y "*" -> fun(x: Int, y: Int) = x * y "/" -> fun(x: Int, y: Int) = if (y != 0) x / y else throw ArithmeticException("ゼロによる割り算はできません") else -> throw IllegalArgumentException("無効な演算子: $operator") } return arithmeticCalculatorFunction } fun main() { val adder = fourArithmeticCalculatorCreator("+") // 加算器として使える関数 adder を作成したようなイメージ val subtractor = fourArithmeticCalculatorCreator("-") // 減算器として使える関数 subtractor を作成したようなイメージ val multiplier = fourArithmeticCalculatorCreator("*") // 乗算器として使える関数 multiplier を作成したようなイメージ val divider = fourArithmeticCalculatorCreator("/") // 除算器として使える関数 divider を作成したようなイメージ //作成した関数で計算 println(adder(10, 5)) println(subtractor(10, 5)) println(multiplier(10, 5)) println(divider(10, 5)) }
出力)
15
5
50
2
クロージャ (関数閉包)
クロージャとは高階関数とともに用いられることになる概念となりますが、オブジェクトの状態を保持する仕組みになります。
クロージャを使用すると、 関数が定義されたスコープが終了した後でも、 そのスコープ内の変数にアクセスすることができます。
すなわち、関数オブジェクトを返す高階関数を宣言した後に、その関数オブジェクトを変数に代入すると、その変数に代入された関数オブジェクト内で宣言されている変数の値は、変数が変更されるまで保持され続けることになります。
例として、数値を1増やす innerIncrementFunction をローカル関数として持つ高階関数 increment を宣言します。
① で、関数オブジェクトを変数に代入します。 ※ の行は innerIncrementFunction の外部の処理なので、 increment によって関数オブジェクトを作成するときにしか実行されないことを理解するのが重要です。
② で、変数に代入された関数オブジェクト (innerIncrementFunction で定義されている部分がオブジェクトになったもの) を実行します。この段階では、num++ により 1 が返ります。
③ で、再度同じ変数 incrementFunction に代入されている関数オブジェクトを実行します。変数 incrementFunction に代入されたオブジェクトの状態は保持されているので、num++ により 2 が返ります。
④も同様に返りは 3 となります。
fun main() { val incrementFunction = increment() // ① println(incrementFunction()) // ② println(incrementFunction()) // ③ println(incrementFunction()) // ④ } fun increment(): ()->Int{ var num = 0 // ※ val innerIncrementFunction : () -> Int = fun () : Int = num++ return innerIncrementFunction }
実行結果
1
2
3
もう一つの例として、別の変数に同じ高階関数による関数オブジェクトを代入し、それぞれのオブジェクトで保持されている値が干渉しないことを示すケースです。
以下では、incrementFunction と incrementFunction2 に increment() で生成される関数オブジェクトを代入していますが、それぞれ別のオブジェクトで、別々に状態が保持されていますので、それぞれの値は干渉しないことがわかります。
fun main() { val incrementFunction = increment() val incrementFunction2 = increment() println(incrementFunction()) println(incrementFunction()) println(incrementFunction()) println(incrementFunction2()) println(incrementFunction2()) println(incrementFunction2()) } fun increment(): ()->Int{ var num = 0 val innerIncrementFunction : () -> Int = fun () : Int = ++num return innerIncrementFunction }
出力
1
2
3
1
2
3
理解を深めるために、val ではなく再代入可能な var にして、同じ変数にもう一度関数オブジェクトを代入するパターンも記載しておきます。
fun main() { var incrementFunction = increment() println(incrementFunction()) println(incrementFunction()) println(incrementFunction()) incrementFunction = increment() // ⑤ println(incrementFunction()) println(incrementFunction()) println(incrementFunction()) } fun increment(): ()->Int{ var num = 0 val innerIncrementFunction : () -> Int = fun () : Int = ++num return innerIncrementFunction }
出力
何が起こったかというと、⑤ で同じ変数 incrementFunction に対して、新しい関数オブジェクトを生成して代入しています。
そのため、その直前までに 変数 incrementFunction に保持されていたオブジェクトの情報は、代入によって新しいものに置き換わりますので、以下のようになります。
1
2
3
1
2
3