「ズンドコチェック」なるものが流行っているらしい

「習作(study)」としてはなかなか秀逸なアイデアだと思う。 これで満点くれる教官も流石だが(笑) 巷では「ズンドコキヨシ」とか「キヨシチェック」とか「ズンドコチェック」とか呼ばれているらしい。

というわけで

「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了って関数

Go 言語で実装することを考えてみる。 私はコレを「ズンドコ・コール(zundoko-choir)」と呼ぶことにする。

とはいえ,ズンドコ・コールを実装する事自体はそう難しくない。 たとえばこんな感じ。

package main

import (
    "fmt"
    "math/rand"
    "time"
)

const (
    zun     = "ズン"
    doko    = "ドコ"
    kiyoshi = "キ・ヨ・シ!"
)

func generate() chan string {
    ch := make(chan string)
    go func() {
        var zundoko = [2]string{zun, doko}
        rand.Seed(time.Now().UnixNano())
        for {
            ch <- zundoko[rand.Intn(2)]
        }
    }()
    return ch
}

func main() {
    zundoko := generate()
    zcount := 0
    for {
        zd := <-zundoko
        fmt.Print(zd)
        if zd == zun {
            zcount++
        } else if zcount >= 4 {
            break
        } else {
            zcount = 0
        }
    }
    fmt.Print(kiyoshi)
}

「ズン」および「ドコ」をランダムに生成する部分は channelgoroutine を使えばいいだろう(generate() 関数内の処理)。 擬似乱数は厳密でなくてもいいので安直に math/rand を使うことにした1。 さらに「ズン」「ズン」「ズン」「ズン」「ドコ」の配列パターンのチェックだが,「ズン」が4回以上連続で来た直後に「ドコ」が来たら OK としてみた。 まぁ,これがもっとも素朴な実装でパフォーマンスとしてもそれほど遜色ない筈。

と,ここまで考えてハタと気づいた。 問題は「自作関数を作り記述しなさい」なんだからメイン関数にロジック書いたらアカンやん!

というわけでまたもゴリゴリとコードを書いてパッケージにしてしまった。 アホだ,私(笑)

出力は標準出力に直書きするのではなく stringsliceappend() することで実現する。 この出力先を Choirs 型として定義した。

//Choirs - zundoko-choirs list
type Choirs struct {
    c []string
}

//Push is append choirs
func (c *Choirs) Push(s string) {
    c.c = append(c.c, s) //maybe panic if c is nil.
}

func (c *Choirs) String() string {
    if c == nil {
        return ""
    }
    content := make([]byte, 0, 128)
    for _, s := range c.c {
        content = append(content, s...)
    }
    return string(content)
}

ちなみに文字列の連結は strings.Join() 関数は使わず「文字列連結はどれが速い?」で紹介した方法を使っている。

これで最初のコードは

func generate() chan string {
    ch := make(chan string)
    go func() {
        var zd = [2]string{Zun, Doko}
        rand.Seed(time.Now().UnixNano())
        for {
            ch <- zd[rand.Intn(2)]
        }
    }()
    return ch
}

//Run zundoko-choirs
func Run() *Choirs {
    zd := generate()
    c := &Choirs{make([]string, 0)}
    zcount := 0
    for {
        s := <-zd
        c.Push(s)
        if s == Zun {
            zcount++
        } else if zcount >= 4 {
            break
        } else {
            zcount = 0
        }
    }
    c.Push(Kiyoshi)
    return c
}

と書き換えることができる。 このパッケージを呼び出すメイン側は例えば

package main

import (
    "fmt"

    "github.com/spiegel-im-spiegel/zundoko"
)

func main() {
    c := zundoko.Run()
    fmt.Println(c)
}

と書けばいい。

ところで「ズン」「ドコ」の出力は Choirs 型で保持られているので,末尾の5要素のパターンを調べる別の方法もあると気づく。 たとえばこんな感じ。

var matchingPattern = []string{Zun, Zun, Zun, Zun, Doko}

func (c *Choirs) match() bool {
    if c == nil {
        return false
    }
    if len(c.c) < 5 {
        return false
    }
    return reflect.DeepEqual(c.c[len(c.c)-5:], matchingPattern)
}

この関数を使えば Run() 関数は

//Run2 zundoko-choirs (another logic)
func Run2() *Choirs {
    zd := generate()
    c := &Choirs{make([]string, 0)}
    for {
        s := <-zd
        c.Push(s)
        if c.match() {
            break
        }
    }
    c.Push(Kiyoshi)
    return c
}

となり随分すっきりする。 ただこれコストが高くつきそうである。 というわけで,これも調べてみた。 まず以下のベンチマークを書く。

package zundoko

import "testing"

func BenchmarkRun1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Run()
    }
}

func BenchmarkRun2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Run2()
    }
}

BenchmarkRun1 が従来のもの, BenchmarkRun2 が先程の match() 関数を使ったバージョンである。 結果は以下のとおり。

$ go test -bench Run -benchmem
testing: warning: no tests to run
PASS
BenchmarkRun1-4    50000     28141 ns/op    1800 B/op     9 allocs/op
BenchmarkRun2-4    30000     40102 ns/op    3912 B/op   115 allocs/op
ok      github.com/spiegel-im-spiegel/zundoko   4.261s

乱数の要素が絡むので毎回同じ値ではないが,傾向としてはこんな感じ。 BenchmarkRun2 のほうが allocation 回数が圧倒的に多いのが分かるだろう。 これがスピードにもダイレクトに反映されている感じである。

今回は「「ズン」が4回以上連続で来た直後に「ドコ」が来たら OK」という単純なロジックだったが,もっと複雑なパターンが要求される場合は工夫が必要かもしれない2

「ズン」と「ドコ」の出現回数を数える関数も作ってみた。

//CountZunDoko returns count of "ZUN" and "DOKO" choirs
func (c *Choirs) CountZunDoko() (int, int) {
    z := 0
    d := 0
    if c == nil {
        return z, d
    }
    for _, s := range c.c {
        switch s {
        case Zun:
            z++
        case Doko:
            d++
        }
    }
    return z, d
}

たとえば generate() 関数内で使っている擬似乱数パッケージを別のものに換えた時に統計処理で簡単な性能評価ができるかもしれない。 今回はそこまではしなけど(擬似乱数の話はいずれやりたい)。

こうやって手遊びでコードを弄るのは楽しいものである。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. math/rand の乱数生成アルゴリズムの既定は線形合同法らしい。線形合同法は性能が良くなくゲームや暗号等では使えない。math/rand の乱数生成アルゴリズムは他のものに入れ替えることができる。たとえば seehuhn/mt19937 パッケージが使える。 ↩︎

  2. たとえば container/listcontainer/ring といったパッケージを使う手がある。 ↩︎