20190218のKotlinに関する記事は2件です。

サッカー日本代表のスタメンを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 - 1Int同士の演算により -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があったとして、FooInt引数を一つ取る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)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin 1.3のCoroutineのキャンセル①(コルーチンをキャンセルする)

検証環境

この記事の内容は、以下の環境で検証しました。

  • Intellij IDEA ULTIMATE 2018.2
  • Kotlin 1.3.0
  • Gradle Projectで作成
  • GradleファイルはKotlinで記述(KotlinでDSL)

準備

詳細は下記の準備を参照してください。
https://qiita.com/naoi/items/8abf2cddfc2cb3802daa

Cancelling 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関数が存在すること
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む