Kotlinのメソッドパラメータおよび高階関数としてのインターフェイス
1. 序章
Kotlinは、機能的な言語、つまり命令型のパラダイムと並んで機能的なパラダイムを可能にする言語になるよう努めています。 関数パラダイムは、プログラムを関数のモザイクとして考え、引数を取り、結果を生成する必要があることを示しています。 機能タイプと機能変数を持つことはパラダイムの重要な部分であり、もちろん、Kotlinはそれをサポートします。
このチュートリアルでは、Kotlinのメソッドパラメーターおよび高階関数としてのインターフェースについて説明します。
2. 高階関数の宣言と呼び出し
Kotlinで高階関数を宣言するのは簡単です。最も明白な方法はラムダを受け入れることです:
fun performAction(action: () -> Unit) {
// Do things before the action
action()
// Do things after the action
}
// Invocation:
performAction { println("Hello, world!") }
この例はあまり機能的ではありません。引数として渡すことができる関数のタイプは、引数を受け入れず、結果を生成しないタイプだけであり、さまざまな副作用にかなり絞り込まれます。
特定の結果を返すことで、もっと面白くしましょう。
typealias MySpecialSupplier = () -> SuppliedType
fun supplyWithTiming(supplier: MySpecialSupplier) =
measureTimedValue { supplier() }
.let {
println("Invocation took ${it.duration}ms") // Or there can be a Metrics call here
it.value // Returning the actual result
}
最後に、潜在的な実装が多数ある場合は、機能インターフェイスを宣言することを検討できます。 あるいは、高階関数の作成者とユーザーは、図書館の作成者と図書館のユーザーなど、異なるグループの人々である可能性があります。 次に、1つの関数とfunキーワードのみを前面に持つインターフェイスを宣言します。
fun interface Mapper {
fun toSupply(name: String, weight: Int): SuppliedType
}
fun verifiedSupplier(name: String, weight: Int, mapper: Mapper): SuppliedType {
println("Some verification")
return mapper.toSupply(name, weight)
}
// Possible invocation:
verifiedSupplier("Sugar", 1) { name, weight -> SuppliedType(name, weight) }
ご覧のとおり、Kotlin SAM変換により、単純なラムダをパラメーターとして渡すことができます。
もちろん、高階関数がラムダパラメーターやその返される型を気にしないほど一般的である場合もあります。 このような場合、次のKotlin標準ライブラリ関数のように汎用型を使用します。
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>
.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}
カリー化または部分適用も可能です。
fun <T> curry(a: T, bifunction: (T, T) -> T): (T) -> T = { b -> bifunction(a, b) }
val plusFour = curry(4) { a, b -> a + b }
assert(plusFour(2) == 6)
これは、関数を呼び出すコードがその関数のすべての引数がどこから来ているのかわからない場合に、関心の分離を細かくするために使用できます。
3. 高階関数の応用
メートル法の計測、ロギング、リソースの使用など、技術的またはインフラストラクチャ上の理由でビジネスロジックを中断しなければならない場合があります。 結果のコードはあまり読みやすくありません。 また、コードのビジネス以外の側面を変更しているときに誤ってビジネスロジックを変更する可能性があるため、より脆弱です。
タイムアウト内にサーバーから画像をダウンロードし、発生する可能性のある例外を処理する必要があるコードについて考えてみましょう。
val imageResponse = try {
clientExecutor.submit(Callable { client.get(imageUrl) })
.get(10L, TimeUnit.SECONDS)
} catch (ex: Exception) {
logger.error("Failed to get image: $imageUrl")
return
}
たくさんのことが起こっています! タスクをエグゼキュータに送信して、タイムアウトを設定できるようにします。 可能性のある例外をキャッチし、ログレコードを作成します。 さらに、このタイムアウトコードは、サービス全体で繰り返される可能性があります。 このコードで小さなパターンを識別し、それらを分解する必要があります。
タイムアウト機能をからかうことから始めましょう:
fun <T> timeout(timeoutSec: Long, supplier: Callable<T>): T =
clientExecutor.submit(supplier).get(timeoutSec, TimeUnit.SECONDS)
これで、タスクにタイムアウトを設定したい場合は、このメソッドを使用できます。 次に、例外ロギングの側面を扱いましょう。
fun <T> successOrLogException(exceptionMsg: String, supplier: () -> T): T? = try {
supplier()
} catch (ex: Exception) {
logger.error(exceptionMsg)
null
}
確かに、関数型プログラミングでは、誤った出力を処理するためのより洗練された方法がありますが、これは高階関数による関心の分離の概念を説明するためだけのものです。 作成した2つの関数をコードに適用してみましょう。
val imageResponse = successOrLogException("Failed to get image: $imageUrl") {
timeout(10L) { client.get(imageUrl) }
} ?: return
論理は同じですが、今では、それらを達成するために必要なすべての基本的な手順を入念にリストするのではなく、単に意図を述べています。
4. 結論
この記事では、関数を引数として受け取り、結果として返す関数を作成する方法について説明しました。 これらの関数は、基本的な命令のリストではなく、一連の宣言型ステートメントとしてコードを記述するのに役立ちます。 宣言型ステートメントは、コードの背後にある理由を開示します。 このようなアプローチは、コードをより読みやすくするのに役立ちます。
いつものように、すべての例はGitHubでから入手できます。