概要
内部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)
}
}