Kotlin / Programming

ザクっと理解するKotlin Higher-order functions (高階関数)

高階関数(Higher-order functions) とは

パラメーターとして関数を受け取る、もしくは戻り値として関数を返す関数のことを指します。
こちらが高階関数の例です。

fun calculate(x: Int, operation: (Int) -> Int): Int {
    return x + operation(2)
}

calculate関数ではパラメーターとして
・x: Int
・operation: Intをパラメーターとして受け取り、Intを返す関数
を設定しています。
このようにパラメーターとして関数を受け取る、もしくは戻り値として関数を返す関数のことを高階関数と呼びます。

関数をパラメーターとして設定する場合、関数型(Function types)を使って記述する必要があります。関数型は、渡す関数がどのような引数を取り、どのような戻り値を返すかを表現する型です。

関数型(Function types) の書き方

・括弧で囲んだパラメーターの型と戻り値の型を書く

(Int, Int) -> Int

この場合、IntとIntをパラメーターとして受け取り、戻り値としてIntを返す関数型を設定しています。

() -> String

(String) -> Unit

上ではパラメーターなしでStringを返す関数型を、下ではStringのパラメーター1つと戻り値がない関数型を指定しています。
戻り値の型を記述する際、Unitは省略できません。


・レシーバータイプを指定する(任意)

Int.(Int) -> Int

このようにすることでレシーバータイプを指定できます。
この場合、Int型のパラメーターを持ち、Int型を戻り値とする関数をInt型のレシーバーオブジェクトで呼び出すことになります。
レシーバータイプに関してはこちらで解説しています。


・サスペンド関数を指定する(suspend関数のみで使用可能)

suspend () -> Unit

suspend修飾子がついた関数を指定する場合は上記のように記述します。
この場合、パラメーターがなく戻り値もないsuspend関数を指定しています。
suspend関数に関してはこちらで解説しています。

高階関数の呼び出し

高階関数のパラメーターに設定されている関数型に適した関数を指定する必要があります。

fun main() {
    val result1 = calculate(5, {x -> x * 2})
    println(result1) // 結果: 9
    
    val result2 = calculate(5, fun(x: Int): Int { return x * 2 })
    println(result2) // 結果: 9
    
    val argument1: (Int) -> Int = {x -> x * 2}
    val result3 = calculate(5, argument1)
    println(result3) // 結果: 9
    
    val argument2: (Int) -> Int = fun(x: Int): Int { return x * 2 }
    val result4 = calculate(5, argument2)
    println(result4) // 結果: 9
}

fun calculate(x: Int, operation: (Int) -> Int): Int {
    return x + operation(2)
}

calculate関数には
・x: Int
・operation: Intのパラメーター1つを受け取り、Intを返す関数
がパラメーターとして設定されています。
result1とresult2の第2引数では関数を、result3とresult4では関数を代入した変数を指定しています。

このように、関数型に適した関数を生成することを関数型のインスタンス化と呼びます。
次のセクションでは関数型のインスタンス化について解説しています。

関数型のインスタンス化

関数型のインスタンス化にはいくつかの方法があります。
(各コード例は下の方にあります)

1. 関数リテラル内のコードブロックを使用する
  ・ラムダ式
  ・無名関数
2. 既存の宣言を参照して使用する
  ・トップレベル、ローカル、メンバー、もしくは拡張した関数
  ・トップレベル、メンバー、もしくは拡張したプロパティ
  ・コンストラクター
3. インターフェースとして関数型を実装しているカスタムクラスのインスタンスを使用する

※関数リテラルとは宣言されていないが式として渡される関数を意味し、ラムダ式や無名関数が関数リテラルの例です。

ラムダ式

fun main() {
    val result1 = calculate(5, {x -> x * 2})
    println(result1) // 結果: 9
}

fun calculate(x: Int, operation: (Int) -> Int): Int {
    return x + operation(2)
}

calculateの第2引数にラムダ式で記述した {x -> x * 2} を指定しています。
ラムダについてはこちらで解説しています。

無名関数

fun main() {
    val result2 = calculate(5, fun(x: Int): Int { return x * 2 })
    println(result2) // 結果: 9
}

fun calculate(x: Int, operation: (Int) -> Int): Int {
    return x + operation(2)
}

こちらは無名関数 fun(x: Int): Int { return x * 2 } で記述しています。

トップレベル、ローカル、メンバー、もしくは拡張した関数

fun main() {
    val result5 = calculate(5, ::multiplyTwo)
    println(result5) // 結果: 9
}

fun multiplyTwo(x: Int): Int {
    return x * 2
}

fun calculate(x: Int, operation: (Int) -> Int): Int {
    return x + operation(2)
}

トップレベルの関数であるmultiplyTwoを第2引数として渡しています。
この場合、「::」を忘れると下記エラーが表示されます。

Function invocation ‘multiplyTwo(…)’ expected
訳: multiplyTwo関数呼び出しには引数を設定してください

これは「::」を省くと、コンパイラがmultiplyTwoを普通の関数呼び出しとして処理してしまうため発生します。「::」はコンパイラに関数型のインスタンスとして扱うように依頼するためにつけていると思われます。

トップレベル、メンバー、もしくは拡張したプロパティ

val topLevelProperty01: (Int) -> Int = {x -> x * 2}

fun main() {
    val result6 = calculate(5, topLevelProperty01)
    println(result6) // 結果: 9
}

fun calculate(x: Int, operation: (Int) -> Int): Int {
    return x + operation(2)
}

トップレベルプロパティであるtopLevelProperty01を引数として渡しています。

コンストラクター

fun main() {
    val class01 = Class01({x -> x * 2})
    println(class01.function01(5)) // 結果: 9
}

class Class01(val operation: (Int) -> Int) {
    fun function01(x: Int): Int {
        return x + operation(2)
    }
}

Class01には、関数型(Int) -> Intを引数として受け取り、operationプロパティに設定するコンストラクターがあります。
クラスのインスタンス化時に、operationプロパティにラムダ式 {x -> x * 2} が渡され、関数型がインスタンス化されます。

インターフェースとして関数型を実装しているカスタムクラスのインスタンス

fun main() {
    val class02 = Class02()
    println(class02(2)) // 結果: 4
}

class Class02: (Int) -> Int {
    override fun invoke(x: Int): Int {
        return x * 2
    }
}

Class02は(Int) -> Intを実装しており、そのinvoke()メソッドをオーバーライドしています。
mainでClass02のインスタンスを作成し、それを関数のように呼び出しています。
これにより、Class02のinvoke()が呼び出され、与えられた引数を2倍にして返しています。


上記全てに関して、十分な情報があればコンパイラが型推論を行うことができます。
例えば下記のように、引数 x の型を明示することでコンパイラが型推論可能になります。

val property01 = {x: Int -> x * 2}

型推論ができない場合、下記のようなエラーが出力されます。

Cannot infer a type for this parameter. Please specify it explicitly.
訳: このパラメータの型を推論できません。明示的に指定してください。

このように表示された場合は、型推論できるように情報を記述するか型自体を記述する必要があります。


(引数の型) -> 戻り値の型 の関数型リテラルは、レシーバーの型.(引数の型) -> 戻り値の型 の関数型リテラルに代入でき、その逆も同様にできます。つまり、関数型リテラルは、レシーバーを持つかどうかにかかわらず、同じ関数型として扱うことができます。

fun main() {
    val property02: Int.(Int) -> Int = {x -> this + x * 2}
    println(5.property02(2)) // 結果: 9
    println(property02(5, 2)) // 結果: 9
}

property02に代入された関数型リテラルは、Int型のレシーバーとInt型の引数を取り、Int型の結果を返す関数です。

最初のprintln文では、5をレシーバーとして渡し、その後に2を引数として渡しています。
レシーバーが5であるため、この関数は5に対して引数2を2倍した値である4を足し、結果として9を返します。

2番目のprintln文では、レシーバーがなく、代わりに最初の引数として5を渡し、次の引数として2を渡しています。
この場合、5がレシーバーとして扱われ、前と同じように関数が実行され、9が出力されます。

関数型インスタンスの呼び出し

今までのコードで既に出てきていますが、関数型インスタンスの呼び出し方法はいくつかあります。
またレシーバーオブジェクトの有無で使用可能な呼び出し方法も変わってきます。

レシーバーオブジェクトがない場合

・関数型インスタンス名.(引数)
・関数型インスタンス名.invoke(引数)

val property03: (Int) -> Int = {x -> x * 2}
println(property03(2))
println(property03.invoke(2))

レシーバーオブジェクトがある場合

・関数型インスタンス名.(レシーバーオブジェクト, 引数)
・関数型インスタンス名.invoke(レシーバーオブジェクト, 引数)
・レシーバーオブジェクト.関数型インスタンス名(引数)

val property04: Int.(Int) -> Int = {x -> this + x * 2}
println(property04(5, 2))
println(property04.invoke(5, 2))
println(5.property04(2))

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

新しい言語Mojoとは?

2023年5月13日