- 投稿日:2019-02-18T23:17:28+09:00
サッカー日本代表のスタメンをKotlin DSLで記述する
概要
内部DSLを記述するのに向いている静的型付け言語、といえば
Scala
ですが、Kotlin
もなかなかな柔軟性の高い言語機能を持っています。
書籍『Kotlinイン・アクション』の第11章に高度なテクニックが幾つか紹介されていますが、それらを使ったサンプルを作ってみました。
以下のようなDSLでフォーメーションとスターティングイレブンを記述します。fun main(args: Array<String>) { val eleven = (formation / 4 - 5 - 1) { FW += "大迫" MF += "原口" / "南野" / "堂安" / "遠藤" / "柴崎" DF += "長友" / "吉田" / "冨安" / "酒井" GK += "権田" } format """ | FW-1 |MF-1 MF-2 MF-3 | MF-4 MF-5 |DF-1 DF-2 DF-3 DF-4 | GK-1 |""".trimMargin() println(eleven) }出力は以下のようになります。(何ら実用性のないプログラムですが、サンプルなのでそこはご了承ください...)
大迫 原口 南野 堂安 遠藤 柴崎 長友 吉田 冨安 酒井 権田以下、使っているテクニックを説明します。
演算子のオーバーロード
(formation / 4 - 5 - 1)上記のように、文字列リテラルを使わずにフォーメーションを直にコードとして表現したかったのですが、普通に考えると
4 - 5 - 1
はInt
同士の演算により-2
と解釈されてしまいます。
それを回避するため、左側に演算子/
を置いています。上のコードを冗長に書くと、( ( ( ( formation / 4 ) - 5 ) - 1 ) )となります。
では、最も内側の括弧内は演算はいったい何なのでしょうか。
その正体は、formation
という名前のオブジェクトに定義されたdiv
メソッドです。object formation { infix operator fun div(df: Int) = FormationHelper1(df) }
Kotlin
における演算子のオーバーロードは規約ベースとなっています(特定の名前、シグネチャのメソッドを実装する)。
/
をオーバーロードするには、operator
修飾子を付けてdiv
メソッドを実装します。また、中置演算を可能とするためにinfix
も指定します。即ち、formation./(4)
ではなくformation / 4
という書き方を可能とします。そして、
div
メソッドの戻り値はFormationHelper1
というヘルパークラスのオブジェクトとなっています。class FormationHelper1(private val df: Int) { infix operator fun minus(mf: Int) = FormationHelper2(df, mf) }このヘルパークラスでは、
minus
メソッドを実装することで、-
演算をオーバーロードしています。
なので、formation / 4 - 4
と書けます。
このminus
メソッドの戻り値のFormationHelper2
も同様の実装となっています。class FormationHelper2(private val df: Int, private val mf: Int) { infix operator fun minus(fw: Int) = FormationHelper3(df, mf, fw) }invoke、拡張関数型、レシーバ付きラムダ
その次のラムダ式は一体何なのでしょうか。
(formation / 4 - 5 - 1) { /* このラムダは? */ }まず、
(formation / 4 - 5 - 1)
の部分はFormationHelper3
クラスのオブジェクトです(FormationHelper2#minus
の戻り値)。演算子の優先順位の都合で、括弧で括っています。その後ろのラムダ式が渡されるメソッドの実体は、以下の
invoke
メソッドです。class FormationHelper3(private val numOfDf: Int, private val numOfMf: Int, private val numOfFw: Int) { // ..(略).. operator fun invoke(eleven: FormationHelper3.() -> Unit): FormationHelper3 { this.eleven() return this }
operator
修飾子が付いていることから推測できるかと思いますが、これも規約ベースでのオーバーロードを行っています。
何をオーバーロードしているかというと、オブジェクトの関数的呼び出しです。
例えば、Foo
クラスのオブジェクトを格納する変数foo
があったとして、Foo
がInt
引数を一つ取るinvoke
メソッドを実装していたならば、foo(1)
はfoo.invoke(1)
と等値となります。上記の
FormationHelper3#invoke
の場合は、ラムダ式を一つ引数で受け取ります。
ただ、その引数の型の指定がちょっと見慣れない感じです。FormationHelper3.() -> Unitこれは拡張関数型と呼ばれるものです。拡張関数型を使うと、レシーバ付きラムダというラムダ式を渡せるようになります。
レシーバ付きというのは、ラムダ式内でのメソッド呼び出しを受け取るオブジェクト(〜レシーバ)が紐づけられている、というイメージです。上記の場合、FormationHelper3
型のオブジェクトをレシーバとし、引数なし、戻り値はUnit型であるラムダを受け取る拡張関数型、と読めます。そして、実際にレシーバを紐づけて拡張関数型を実行するためには、以下のように
レシーバ.関数()
と記述します。この例ではたまたまレシーバ型が自分自身だったのでthis
をレシーバとしていますが、別のオブジェクトでも構いません。this.eleven()そして、呼び出し側のレシーバ付きラムダは以下のようになっています。
(formation / 4 - 5 - 1) { FW += "大迫" MF += "原口" / "南野" / "堂安" / "遠藤" / "柴崎" DF += "長友" / "吉田" / "冨安" / "酒井" GK += "権田" }このラムダ式内では、
this
がレシーバオブジェクトを参照し、各メソッド呼び出しやプロパティアクセスはレシーバに対するメッセージと解釈されます。
上記はthis
を省略していますが、明示的に書くと以下となります。(formation / 4 - 5 - 1) { this.FW += "大迫" this.MF += "原口" / "南野" / "堂安" / "遠藤" / "柴崎" this.DF += "長友" / "吉田" / "冨安" / "酒井" this.GK += "権田" }※なお、
FormationHelper3#invoke
メソッドの唯一の引数がラムダ式であるため、ラムダ式を外に括り出し、空になった( )
は省略可能であることに注意してください。省略せずに書くと、以下のように冗長な感じになります。(formation / 4 - 5 - 1) ({ this.FW += "大迫" this.MF += "原口" / "南野" / "堂安" / "遠藤" / "柴崎" this.DF += "長友" / "吉田" / "冨安" / "酒井" this.GK += "権田" })その他
MF += "原口" / "南野" / "堂安" / "遠藤" / "柴崎"
+=
はplusAssign
メソッド実装による演算子オーバーロードです。
右辺は、String
を連結してList<String>
を生成するように、拡張関数を定義しています(前述のdiv
メソッド実装により/
演算子をオーバーロード)。infix operator fun String.div(other: String) = listOf(this, other) infix operator fun List<String>.div(other: String) = this + otherその他細かい部分は、全ソースコードを添付するのでそちらを参照ください。
全ソースコード
動かしてみたい方は、Try Kotlinに貼り付けて実行可能です。
fun main(args: Array<String>) { val eleven = (formation / 4 - 5 - 1) { FW += "大迫" MF += "原口" / "南野" / "堂安" / "遠藤" / "柴崎" DF += "長友" / "吉田" / "冨安" / "酒井" GK += "権田" } format """ | FW-1 |MF-1 MF-2 MF-3 | MF-4 MF-5 |DF-1 DF-2 DF-3 DF-4 | GK-1 |""".trimMargin() println(eleven) } infix operator fun String.div(other: String) = listOf(this, other) infix operator fun List<String>.div(other: String) = this + other object formation { infix operator fun div(df: Int) = FormationHelper1(df) } class FormationHelper1(private val df: Int) { infix operator fun minus(mf: Int) = FormationHelper2(df, mf) } class FormationHelper2(private val df: Int, private val mf: Int) { infix operator fun minus(fw: Int) = FormationHelper3(df, mf, fw) } class FormationHelper3(private val numOfDf: Int, private val numOfMf: Int, private val numOfFw: Int) { private var keepers: List<String> = emptyList() private var defenders: List<String> = emptyList() private var midfielders: List<String> = emptyList() private var forwards: List<String> = emptyList() val FW: FormationHelper4 get() = FormationHelper4({xs -> forwards += xs}) val MF: FormationHelper4 get() = FormationHelper4({xs -> midfielders += xs}) val DF: FormationHelper4 get() = FormationHelper4({xs -> defenders += xs}) val GK: FormationHelper4 get() = FormationHelper4({xs -> keepers += xs}) operator fun invoke(eleven: FormationHelper3.() -> Unit): FormationHelper3 { this.eleven() return this } infix fun format(template: String): String { val regex = """(FW|MF|DF|GK)-([1-9])""".toRegex() var result = template regex.findAll(template) .map { it.groupValues } .map { Pair(it[0], playerName(it[1], it[2].toInt())) } .forEach { result = result.replace(it.first, it.second) } return result } private fun playerName(position: String, index: Int): String { val (list, max) = when (position) { "FW" -> Pair(forwards, numOfFw) "MF" -> Pair(midfielders, numOfMf) "DF" -> Pair(defenders, numOfDf) "GK" -> Pair(keepers, 1) else -> throw Exception("Invalid position ${position}") } if (index > max) { throw Exception("Too many players (${index}) in ${position} ") } return list[index - 1] } } class FormationHelper4(val callback: (List<String>) -> Unit) { infix operator fun plusAssign(player: String): Unit { callback(listOf(player)) } infix operator fun plusAssign(players: List<String>): Unit { callback(players) } }
- 投稿日:2019-02-18T09:36:37+09:00
Kotlin 1.3のCoroutineのキャンセル①(コルーチンをキャンセルする)
検証環境
この記事の内容は、以下の環境で検証しました。
- Intellij IDEA ULTIMATE 2018.2
- Kotlin 1.3.0
- Gradle Projectで作成
- GradleファイルはKotlinで記述(KotlinでDSL)
準備
詳細は下記の準備を参照してください。
https://qiita.com/naoi/items/8abf2cddfc2cb3802daaCancelling coroutine execution
これまでの記事でキャンセルも少しだけふれましたが、今回からはキャンセルの詳細について説明していきます。
それでは、読み進めます。
In a long-running application you might need fine-grained control on your background coroutines.
For example, a user might have closed the page that launched a coroutine and now its result is no longer needed and its operation can be cancelled.
The launch function returns a Job that can be used to cancel running coroutine:意訳込みですが、訳すと以下のとおりです。
長時間アプリを実行していると、バックグラウンドで実行しているコルーチンを制御する必要がでてきます。
例えば、ユーザが対象のページを閉じてしまった場合、コルーチンは結果を返す必要がなくなります。そして、そのような場合、キャンセルできます。
コルーチンを起動する関数は、コルーチンをキャンセルできるJobを返します。そうでしたね。確かJobオブジェクトが取得できるんでした。
サンプルコードを確認してみましょう。
import kotlinx.coroutines.* fun main() = runBlocking { //sampleStart val job = launch { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } } delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancel() // cancels the job job.join() // waits for job's completion println("main: Now I can quit.") //sampleEnd }launch関数の戻り値でジョブを取得してますね。
その後、cancel関数を呼び出してキャンセルしています。
実行してみるとこんな結果になります。I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... main: I'm tired of waiting! main: Now I can quit.しっかりキャンセルされていることが確認できます。
更に読み進めると。。。
As soon as main invokes job.cancel, we don't see any output from the other coroutine because it was cancelled.
There is also a Job extension function cancelAndJoin that combines cancel and join invocations.訳してみると
mainがcancel関数を呼びだすと出力がキャンセルされます。
JobにはcancelAndJoinという関数が存在します。cancelとjoinを組み合わせた動作をします。まとめ
このブロックで理解できたことは以下のことだと思います。
- Jobのcancel関数を呼び出すとコルーチンが中断する
- cancelAndJoin関数が存在すること