LoginSignup
6
4

More than 5 years have passed since last update.

ズンドコキヨシ with Kotlin

Last updated at Posted at 2018-07-25

はじめに

この記事は Kotlin、なかでもその標準ライブラリーに含まれる kotlin.sequences パッケージの機能を用いて、ズンドコキヨシを実装してみたというものである。

以前「ズンドコキヨシ with C#」という記事を書いた。C#LINQ を用いてズンドコキヨシを実装したというものだ。
筆者は最近になってようやく本格的に Kotlin を使い始めて、Kotlin に kotlin.sequences という LINQ のようなことができるパッケージがあることを知った。そこでそれを用いて「ズンドコキヨシ with C#」をそれで書き直してみた、というのが本記事の内容である。

実行結果例

先に実行結果例を見ておこう。

ドコ
ズン
ドコ
ズン
ズン
ズン
ズン
ズン
ドコ
キ・ヨ・シ!

スタンダードなズンドコである。

アプリケーションコード

続いてアプリケーションコード。

Main.kt
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 は前行で返された「ズン」と「ドコ」の配列である。

generateSequenceSequence のインスタンスを生成する。この Sequence は要素が要求されるたびに与えられた関数を呼び出し、その返値をその要素の値とする。
ここでは「与えられた関数」とは先ほどの「ズン」と「ドコ」のいずれかをランダムに選択して返すという処理を行うラムダ式であるから、この行では「ズン」と「ドコ」をランダムに繰り返す Sequence を生成するということになる。

                    .takeUntilMatchingPattern(PATTERN) // ZUN, ZUN, ZUN, ZUN, DOKO というパターンが現れるまで繰り返す。

takeUntilMatchingPattern は自作のユーティリティー拡張関数である。Sequence をレシーバーとし、引数で与えられたパターンが現れるまでレシーバーからの要素を返すという Sequence を生成する。
つまり、この行が返す Sequence は、末尾が「ズン」「ズン」「ズン」「ズン」「ドコ」で終わる。

                    .map { it.toString() } // 列挙子を文字列に変換する。

mapSequence の拡張関数であり、与えられた関数を用いて Sequence の各要素を変換する。ここでは、前行で生成した Sequence の各要素である Element クラスのインスタンスを、toString メソッドを用いて文字列に変換している。つまり、 Element.ZUNElement.DOKO から成る Sequence だったのを "ズン""ドコ" から成る Sequence に変換している。

                    .plus(KIYOSHI) // 末尾に "キ・ヨ・シ!" を追加する。 `+ KIYOSHI` だと演算子の結合順位上問題がある。

plus は演算子 + のオーバーロードであり、Sequence の末尾に1つ要素を追加する。ここでは文字列 "キ・ヨ・シ!" を追加している。

+ 演算子のオーバーロードなので + KIYOSHI と書くこともできるのだが、このコードの場合はそうすると前の行と + KIYOSHI の結合よりも KIYOSI と次の行の .forEach の結合の方が優先されてしまうため、このようにした。

                    .forEach { println(it) } // 各要素を1行ずつに出力する。

最後、各要素を1行ずつに出力している。

ユーティリティーコード

再利用性が高いユーティリティー関数として、chooseOneAtRandomtakeUntilMatchingPattern を実装した。いずれも拡張関数である。

chooseOneAtRandom には乱数生成オブジェクトを引数で指定できるのだが、省略もできるようにした。
この関数は繰り返し呼ばれるため、省略時に使用されるオブジェクトを毎回生成していては負担が大きい。そのため、乱数生成オブジェクトを1つ持っておき、それを使い回すようにした。このオブジェクトは必要となるまで生成されない。

Utilities.kt
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#」とは一部設計を変えたが、基本的な構成は同じである。見比べてみるのも面白いだろう。

6
4
1

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
6
4