はじめに
この記事は Kotlin、なかでもその標準ライブラリーに含まれる kotlin.sequences
パッケージの機能を用いて、ズンドコキヨシを実装してみたというものである。
以前「ズンドコキヨシ with C#」という記事を書いた。C# の LINQ を用いてズンドコキヨシを実装したというものだ。
筆者は最近になってようやく本格的に Kotlin を使い始めて、Kotlin に kotlin.sequences
という LINQ のようなことができるパッケージがあることを知った。そこでそれを用いて「ズンドコキヨシ with C#」をそれで書き直してみた、というのが本記事の内容である。
実行結果例
先に実行結果例を見ておこう。
ドコ
ズン
ドコ
ズン
ズン
ズン
ズン
ズン
ドコ
キ・ヨ・シ!
スタンダードなズンドコである。
アプリケーションコード
続いてアプリケーションコード。
package zundokokiyoshi
fun main(args: Array<String>) = ZunDokoKiyoshi().play()
/** ズンドコキヨシのロジック。 */
class ZunDokoKiyoshi {
/** 繰り返す要素。 */
enum class Element {
ZUN, DOKO;
override fun toString() = when (this) {
Element.ZUN -> "ズン"
Element.DOKO -> "ドコ"
}
}
companion object {
/** このパターンが現れるまで繰り返す。 */
val PATTERN = arrayOf(
Element.ZUN, Element.ZUN, Element.ZUN,
Element.ZUN, Element.DOKO)
/** 最後に出力する文字列。 */
val KIYOSHI = "キ・ヨ・シ!"
}
/** ズンドコキヨシを実行する。 */
fun play() =
Element.values() // enumValues<Element>() と書いてもよい。
.let { generateSequence { it.chooseOneAtRandom() } } // ZUN, DOKO のいずれかをランダムに繰り返す。
.takeUntilMatchingPattern(PATTERN) // ZUN, ZUN, ZUN, ZUN, DOKO というパターンが現れるまで繰り返す。
.map { it.toString() } // 列挙子を文字列に変換する。
.plus(KIYOSHI) // 末尾に "キ・ヨ・シ!" を追加する。 `+ KIYOSHI` だと演算子の結合順位上問題がある。
.forEach { println(it) } // 各要素を1行ずつに出力する。
}
ごく短い main 関数と、小さくシンプルなクラス1つにまとまった。(後述するが、これ以外には再利用性が高いユーティリティーコードだけである。)
上から順に説明していこう。
main 関数
fun main(args: Array<String>) = ZunDokoKiyoshi().play()
main 関数。ZunDokoKiyoshi
クラスのインスタンスを生成して実行しているだけである。
繰り返し要素の定義
/** 繰り返す要素。 */
enum class Element {
ZUN, DOKO;
override fun toString() = when (this) {
Element.ZUN -> "ズン"
Element.DOKO -> "ドコ"
}
}
繰り返し要素である「ズン」と「ドコ」を列挙値として定義している。
文字列に変換する処理もメソッドとして持たせているが、これは拡張関数などとして別に定義しても良いだろう。
「ズン」「ズン」「ズン」「ズン」「ドコ」のパターンと「キ・ヨ・シ!」の定義
companion object {
/** このパターンが現れるまで繰り返す。 */
val PATTERN = arrayOf(
Element.ZUN, Element.ZUN, Element.ZUN,
Element.ZUN, Element.DOKO)
/** 最後に出力する文字列。 */
val KIYOSHI = "キ・ヨ・シ!"
}
「ズン」or「ドコ」の繰り返しをやめて「キ・ヨ・シ!」を出力するための条件となるパターン(「ズン」「ズン」「ズン」「ズン」「ドコ」)を定義している。
また、「キ・ヨ・シ!」の文字列も定義している。(こちらはもう少し抽象化した定数名にしたほうが良いかもしれない。)
ズンドコキヨシを実行するメソッド
/** ズンドコキヨシを実行する。 */
fun play() =
「ズンドコキヨシ」を実行するメソッドである。
ここからは1行ずつ解説していく。
Element.values() // enumValues<Element>() と書いてもよい。
「ズン」(Element.ZUN
)と「ドコ」(Element.DOKO
)を要素に持つ配列を作っている。
.let { generateSequence { it.chooseOneAtRandom() } } // ZUN, DOKO のいずれかをランダムに繰り返す。
chooseOneAtRandom
は自作のユーティリティー拡張関数である。配列をレシーバーとし、その配列の要素のうちから1つをランダムに選択して返す。このコードではレシーバーは it
であり、この it
は前行で返された「ズン」と「ドコ」の配列である。
generateSequence
は Sequence
のインスタンスを生成する。この Sequence
は要素が要求されるたびに与えられた関数を呼び出し、その返値をその要素の値とする。
ここでは「与えられた関数」とは先ほどの「ズン」と「ドコ」のいずれかをランダムに選択して返すという処理を行うラムダ式であるから、この行では「ズン」と「ドコ」をランダムに繰り返す Sequence
を生成するということになる。
.takeUntilMatchingPattern(PATTERN) // ZUN, ZUN, ZUN, ZUN, DOKO というパターンが現れるまで繰り返す。
takeUntilMatchingPattern
は自作のユーティリティー拡張関数である。Sequence
をレシーバーとし、引数で与えられたパターンが現れるまでレシーバーからの要素を返すという Sequence
を生成する。
つまり、この行が返す Sequence
は、末尾が「ズン」「ズン」「ズン」「ズン」「ドコ」で終わる。
.map { it.toString() } // 列挙子を文字列に変換する。
map は Sequence
の拡張関数であり、与えられた関数を用いて Sequence
の各要素を変換する。ここでは、前行で生成した Sequence
の各要素である Element
クラスのインスタンスを、toString
メソッドを用いて文字列に変換している。つまり、 Element.ZUN
と Element.DOKO
から成る Sequence
だったのを "ズン"
と "ドコ"
から成る Sequence
に変換している。
.plus(KIYOSHI) // 末尾に "キ・ヨ・シ!" を追加する。 `+ KIYOSHI` だと演算子の結合順位上問題がある。
plus は演算子 +
のオーバーロードであり、Sequence
の末尾に1つ要素を追加する。ここでは文字列 "キ・ヨ・シ!"
を追加している。
+
演算子のオーバーロードなので + KIYOSHI
と書くこともできるのだが、このコードの場合はそうすると前の行と + KIYOSHI
の結合よりも KIYOSI
と次の行の .forEach
の結合の方が優先されてしまうため、このようにした。
.forEach { println(it) } // 各要素を1行ずつに出力する。
最後、各要素を1行ずつに出力している。
ユーティリティーコード
再利用性が高いユーティリティー関数として、chooseOneAtRandom
と takeUntilMatchingPattern
を実装した。いずれも拡張関数である。
chooseOneAtRandom
には乱数生成オブジェクトを引数で指定できるのだが、省略もできるようにした。
この関数は繰り返し呼ばれるため、省略時に使用されるオブジェクトを毎回生成していては負担が大きい。そのため、乱数生成オブジェクトを1つ持っておき、それを使い回すようにした。このオブジェクトは必要となるまで生成されない。
package zundokokiyoshi
import java.util.*
internal val random by lazy { Random() }
/** 配列から要素を1つランダムに選択する。 */
fun <T> Array<T>.chooseOneAtRandom(
random: Random = zundokokiyoshi.random
): T = this[random.nextInt(size)]
/**
* 指定されたパターンにマッチするまでの要素を含むシーケンスを返す。
*
* 返すシーケンスの末尾は指定されたパターンにマッチする。
*/
fun <T> Sequence<T>.takeUntilMatchingPattern(
pattern: Array<T>
): Sequence<T> = sequence {
// パターンが空であれば、最初からマッチしており、そこまでに含まれる要素も空である。
if (pattern.isEmpty()) {
return@sequence
}
val iterator = iterator()
// 途中で要素を変更されても影響がないようにコピーするとともに、
// queue と一致しているかどうかを調べやすくするため List に変換する。
val pattern = pattern.toList()
// レシーバーの要素の末尾から (pattern.size) 個を保持するキュー。
val queue: Queue<T> = LinkedList<T>()
while (iterator.hasNext()) {
val element = iterator.next()
yield(element)
// マッチしたかどうかの判定を行う。
if (queue.size == pattern.size) {
queue.remove()
}
queue.add(element)
if (queue == pattern) {
return@sequence
}
}
}
おわりに
ズンドコキヨシを Kotlin で C# の LINQ 風に書いてみたが、いかがだっただろうか。
「ズンドコキヨシ with C#」とは一部設計を変えたが、基本的な構成は同じである。見比べてみるのも面白いだろう。