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