2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

サッカー日本代表のスタメンをKotlin DSLで記述する

Posted at

概要

内部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)
    }
}
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?