Go
golang

[Go言語] もしも童話「シンデレラ」が、Goで書かれていたら

シンデレラがGoで書かれていたら。
という妄想です。
少し長いですがお付き合いください。

TL;DR

cinderella.gif

ソースはここにおいてます
https://github.com/lboavde1121/cinderella

あらすじ

以下あらすじ。

  • シンデレラは、継母とその連れ子である姉たちに日々いじめられていた。
  • あるとき、城で舞踏会が開かれ、姉たちは着飾って出ていくが、シンデレラにはドレスがなかった。
  • 舞踏会に行きたがるシンデレラを、不可思議な力(魔法使い、仙女、ネズミ、母親の形見の木、白鳩など)が助け、準備を整えるが、魔法は午前零時に解けるので帰ってくるようにと警告される。
  • シンデレラは、城で王子に見初められる。
  • 零時の鐘の音に焦ったシンデレラは階段に靴を落としてしまう。
  • 王子は、靴を手がかりにシンデレラを捜す。
  • 姉2人も含め、シンデレラの落とした靴は、シンデレラ以外の誰にも合わなかった。
  • シンデレラは王子に見出され、妃として迎えられる。

※Wikipediaからの引用
シンデレラ - Wikipedia

シンデレラ ver Go

はじめに

登場人物構造体を定義する

Go言語には他言語で使用されることの多いclassが存在しないので、
構造体を使用して登場人物を表現します。

actor.go
package main

import "fmt"

type Gender int

const (
    _ Gender = iota
    Woman
    Man
)

// 登場人物用
type Human struct {
    Name   string
    Age    int
    Gender Gender
    Cos    Costume
    Shoes  *Shoes
}

func NewHuman(n string, age int, g Gender) *Human {
    return &Human{
        Name:   n,
        Age:    age,
        Gender: g,
    }
}

func (h *Human) Say(s string) {
    fmt.Printf("%v: %v\n", h.Name, s)
}

func (h *Human) SetCostume(c Costume) {
    h.Cos = c
}
func (h *Human) SetShoes(s *Shoes) {
    h.Shoes = s
}

Human構造体を、NewHuman関数(コンストラクタ)でインスタンスを生成します。
このNewHuman関数を使用して、Humanという定義から実体(シンデレラとか王子とか)を生成します。

定数の連番整数を生成

const (
    _ Gender = iota // _ Gender     = 0
    Woman           // Woman Gender = 1
    Man             // Man Gender   = 2
)

iotaを使用することで連番の整数を生成することができます。
iotaはconst内でしか使用することができないので注意してください。

衣装用構造体の定義

シンデレラのメインステージとなる?舞踏会に参加するにはドレスコードがあるようです。
調べるのも面倒なので、ドレスと燕尾服のみ定義しました。
裸じゃなければ大丈夫。

costume.go
package main

// 衣装インタフェース
type Costume interface {
    Wear(h *Human) bool
}

// ドレス
type Dress struct {
    Owner *Human
}

func NewDress(h *Human) Costume {
    return &Dress{
        Owner: h,
    }
}

// 所有者のみ着ることができる
func (d *Dress) Wear(h *Human) bool {
    return h == d.Owner && h.Gender == Woman
}

// 燕尾服
type Tailcoat struct {
    Owner *Human
}

func NewTailcoat(h *Human) Costume {
    return &Tailcoat{
        Owner: h,
    }
}

func (t *Tailcoat) Wear(h *Human) bool {
    return h == t.Owner && h.Gender == Man
}

定義したドレスと燕尾服の構造体ですが、
Costumeというインタフェースの実装となっています。
こうすることで、ドレスであっても燕尾服であっても、
Costumeの持つWear関数(服の持ち主のみ着用できる)の実装を必須にすることができます。

靴の構造体を定義

ガラスの靴も出てくるので
ついでに靴の構造体も作成します。

shoes.go
package main

// 靴
type Shoes struct {
    Owner *Human
}

func NewShoes(h *Human) *Shoes {
    return &Shoes{
        Owner: h,
    }
}

// 所有者のみ履くことができる
func (s *Shoes) Wear(h *Human) bool {
    return h == s.Owner
}

これはCostumeインタフェースの実装ではありません。
正直迷ったのですが、一旦この形になりました。

継母たちと暮らしていたシンデレラ

シンデレラは、継母とその連れ子である姉たちと暮らしていました。
シンデレラは日々いじめられており、まるで召使のように扱われていました。

cinderella.go
stepMother := NewHuman("StepMother", 52, Woman)
sisterA := NewHuman("SisterA", 23, Woman)
sisterB := NewHuman("SisterB", 20, Woman)
cinderella := NewHuman("ella", 18, Woman)

stepMother.Say("今日もいじめてやるw")
sisterA.Say("今日もいじめてやるw")
sisterB.Say("今日もいじめてやるw")
cinderella.Say("・・・")

実行結果

StepMother: 今日もいじめてやるw
SisterA: 今日もいじめてやるw
SisterB: 今日もいじめてやるw
ella: ・・・

Wikipediaに 本来「エラ」という名前なのだが、灰で汚れた姿を継母達が「灰まみれのエラ/シンダーエラ」と馬鹿にしてからかった事から、シンデレラと呼ばれる様になりと書いてあったのでエラという名前にしています。
姉たちは名前がわからなかったので適当です。
年齢も適当です。

舞踏会が開催されるらしい

舞踏会の構造体を定義

舞踏会の構造体を定義しなければ舞踏会は始まりません。

ball.go
package main

import (
    "fmt"
    "sync"
)

// 舞踏会
type Ball struct {
    m          sync.Mutex
    Entries    []*Human
    Clock      int // 時刻
    FinishedAt int
}

func NewBall(startedAt, finishedAt int) *Ball {
    return &Ball{
        Clock:      startedAt,  // 開始時刻
        FinishedAt: finishedAt, // 終了時刻
    }
}

func (b *Ball) Start() {
    fmt.Println("舞踏会開始")
}

func (b *Ball) Dancing() {
    b.m.Lock()
    defer b.m.Unlock()
    fmt.Printf("現在 %d時\n", b.Clock)
    for _, h := range b.Entries {
        fmt.Printf("%v は踊っている\n", h.Name)
    }
    b.Clock++
}

func (b *Ball) Finish() {
    fmt.Println("舞踏会は終了")
}

func (b *Ball) IsFinished() bool {
    return b.Clock >= b.FinishedAt
}

func (b *Ball) Entry(h *Human) {
    if h.Cos != nil {
        b.Entries = append(b.Entries, h)
        fmt.Printf("%v は舞踏会に参加します。\n", h.Name)
    } else {
        fmt.Println("衣装を持っていないと参加できません")
        fmt.Printf("%v は舞踏会に参加できない。\n", h.Name)
    }
}

func (b *Ball) Exit(h *Human) {
    b.m.Lock()
    defer b.m.Unlock()
    var entries []*Human
    for _, e := range b.Entries {
        if e != h {
            entries = append(entries, e)
        }
    }
    b.Entries = entries
    fmt.Printf("%v は舞踏会から抜け出し、帰宅した。\n", h.Name)
}

この舞踏会はドレスコードが必須になっているようです。

if h.Cos != nil {
        b.Entries = append(b.Entries, h)
        fmt.Printf("%v は舞踏会に参加します。\n", h.Name)
    } else {

Human構造体のCostumeをチェックし参加制限をしています。

追記(11/19)

@mattn さんからコメントをいただき
競合の可能性があるものを修正していただきました。

type Ball struct {
    m          sync.Mutex
}

// ~ 省略 ~
b.m.Lock()
defer b.m.Unlock()

// ~ 省略 ~

Exit の呼び出しと Dancing の呼び出しが同時に発生した場合、クラッシュする可能性があります。
sync.Mutexを使用して、ロックを行いクラッシュを回避しています。

ちなみに、race conditionの確認は実行時に -race オプションを使用することで確認できます。

$ go run -race ./

race condition の可能性がある場合、warningが出ます。

cgoを有効にしていない場合はこっち。

$ CGO_ENABLED=1; go run -race ./

@mattn さんありがとうございます。

舞踏会が開催!!

cinderella.go
ball := NewBall(19, 27)

コンストラクタに、開始時間と終了時間を渡して、舞踏会インスタンスを作成します。
19時〜27時までって長いですね。

舞踏会に行きたいシンデレラ

継母家のドレスルーム構造体を定義

継母家にはドレスを保管する部屋があるようです。

costume.go
// ドレスルーム
type DressRoom struct {
    Dresses []*Dress
}

func NewDressRoom() *DressRoom {
    return &DressRoom{}
}

// ドレスを収納する
func (d *DressRoom) Store(humans ...*Human) {
    for _, h := range humans {
        cos := NewDress(h)
        if dress, ok := cos.(*Dress); ok {
            d.Dresses = append(d.Dresses, dress)
        }
    }
}

func (dr *DressRoom) GetDress(h *Human) {
    for _, dress := range dr.Dresses {
        if dress.Wear(h) {
            h.SetCostume(dress)
        }
    }
}

可変長引数

関数の引数を

 Store(humans ...*Human) 

↑のようにすることで、可変長引数にすることができます。
Human型であればいくつでも渡すことができます。

DressRoom.Store(stepMother, sisterA, sisterB)

型アサーション

Human構造体のもつCos Costumeは実体がDress型であっても、
Costumeインタフェースの実装として扱われるため、
ドレスしか格納できないドレスルームに格納することができません。
Dress型に型アサーションをおこないDress型として振る舞えるようにします。

if dress, ok := cos.(*Dress); ok {
    d.Dresses = append(d.Dresses, dress)
}

継母たちはドレスの準備をする

cinderella.go
// 舞踏会用のドレスを用意します。
dressRoom := NewDressRoom()
dressRoom.Store(stepMother, sisterA, sisterB)

// シンデレラのドレスは用意しません。

舞踏会へGo

継母たちは舞踏会へと向かいますが、シンデレラはドレスを持っていないため参加することができません。

cinderella.go
// 継母のドレスはある
dressRoom.GetDress(stepMother)
// 舞踏会参加
ball.Entry(stepMother)
// 姉Aのドレスもある
dressRoom.GetDress(sisterA)
ball.Entry(sisterA)
// 姉Bのドレスもある
dressRoom.GetDress(sisterB)
ball.Entry(sisterB)

// シンデレラだけドレスがない。。
dressRoom.GetDress(cinderella)
ball.Entry(cinderella)

実行結果

StepMother は舞踏会に参加します。
SisterA は舞踏会に参加します。
SisterB は舞踏会に参加します。
衣装を持っていないと参加できません
ella は舞踏会に参加できない

どうしても舞踏会へ行きたいシンデレラ。

魔法で準備をしてもらう

そんなシンデレラの元に不思議な力が集まります。

魔法の定義

舞踏会へ行けないシンデレラを可哀想に思ったのか、魔法使いたち(省略)が不思議な力で助けてくれるそうです。

magic.go
package main

import "fmt"

type Magic struct {
    Target *Human
    Broken chan int
}

func NewMagic(h *Human) *Magic {
    return &Magic{
        Target: h,
        Broken: make(chan int, 1),
    }
}

func (m *Magic) GenerateDress() Costume {
    fmt.Printf("%v は魔法でドレスを作ってもらった!\n", m.Target.Name)
    return NewDress(m.Target)
}
func (m *Magic) GenerateGlassShoes() *Shoes {
    fmt.Printf("%v は魔法でガラスの靴を作ってもらった!\n", m.Target.Name)
    return NewShoes(m.Target)
}

魔法の力で、舞踏会に行くための準備をしてくれるようです。

シンデレラは舞踏会へ行く

cinderela.go
magic := NewMagic(cinderella)
cinderella.SetCostume(magic.GenerateDress())
cinderella.SetShoes(magic.GenerateGlassShoes())

魔法の力で舞踏会に行く準備ができました。

魔法は便利なだけではないようで
魔法使いたちはシンデレラに、こう警告します。
「午前零時に解けるので帰ってくるように」

magic.go
func (m *Magic) Limit(limit chan int) {
    <-limit
    fmt.Println("0時が近づいている")
    fmt.Println("魔法が解けそう!!")
    m.Broken <- 1
}
cinderella.go
limit := make(chan int, 1)
go magic.Limit(limit)
// シンデレラ舞踏会へ参加する
ball.Entry(cinderella)

goroutine

ここはGo言語の特徴の一つであるgoroutineを使用して表現しています。
channel型のLimitが値を受信すると魔法がとけてしまうようです。

goroutineを完全に理解して湯水のように使いたい方は以下書籍がオススメです。

O'Reilly Japan - Go言語による並行処理

僕も読んでいる最中です。 
難しくて挫折しそうですが、勉強になります。

チャンネル

goroutineとのデータのやり取りに使用されるのがチャンネルと呼ばれる型です。
これもGo言語の特徴です。
チャンネルは以下のように生成,使用します。

chan1 := make(chan int)    // capacityを指定しない
chan2 := make(chan int, 1) // capacityを指定
chan1 <- 1                 // chan1に値を送信
chan2 <- 1                 // chan2に値を送信
<-chan1                    // chan1から値を受信
<-chan2                    // chan2から値を受信

チャンネルはcapacity(バッファ容量)を指定することができます。
capacityを指定しない場合は容量は0です。
容量を超えるチャンネルへの送信は受信されるまで処理をブロックされます。

chan1 := make(chan int)  // capacityを指定しない
chan1 <- 1               // chan1に値を送信
// chan1 <- 1            // これは容量オーバーなので受信されるまでブロックされる。

また、チャンネル受信は受信するまで処理をブロックします。
そのため、Magic.Limit関数は引数となっているlimitチャンネルの値を受信するまで処理をブロックします。

実行結果

ella は魔法でドレスを作ってもらった!
ella は魔法でガラスの靴を作ってもらった!
ella は舞踏会に参加します。

シンデレラに一目惚れした王子

cinderella.go
// 王子登場
prince := NewHuman("王子", 18, Man)
tailcoat := NewTailcoat(prince)
prince.SetCostume(tailcoat)
ball.Entry(prince)

どうやら、シンデレラに一目惚れしたようですが、省略します。

実行結果

王子 は舞踏会に参加します。

24時が近づいてきて焦って帰るシンデレラ

待ちに待った舞踏会が始まりますが、楽しい時間はあっという間です。

舞踏会が開催

cinderella.go
// 舞踏会開催
ball.Start()
finished := make(chan int, 1)
go func() {
    for !ball.IsFinished() {
        select {
        case <-time.After(1 * time.Second):
            ball.Dancing()
        }
        if ball.Clock == 24 {
            limit <- 1
        }
    }
    ball.Finish()
    finished <- 1
}()

体感時間10秒ほどです。あっという間。

ここもgoroutine+selectで表現しています。
time.Afterのチャンネルを1秒毎に受信し、Dancingで踊らせています。
24時に近くなると、 limitチャンネルに適当な値を送信し、シンデレラの魔法を解除します。
finishedチャンネルを用意し、終了時に適当な値を送信します。

ここの表現もうちょっといい方法ありそう。

select

Go言語のselectは普通は複数のチャンネルを扱うために使用されます。
正直な話をしてしまうと、上のコードはselectを使用せずとも、

for !ball.IsFinished() {
    <-time.After(1 * time.Second):
    ball.Dancing()    
    if ball.Clock == 24 {
        limit <- 1
    }
}

これで行けます。むしろこっちの方が綺麗だと思います。

通常selectを使用するケースは、

package main

import (
    "fmt"
    "time"
)

func doSomething(t time.Time) {
    fmt.Println(t)
}

func main() {
    finished := make(chan int, 1)
    go func() {
        for {
            select {
            case t := <-time.After(1 * time.Second):
                doSomething(t) // 1秒ごとに何かを実行
            case <-finished:
                fmt.Println("終わりだよ〜")
                return
            }
        }
    }()


    time.Sleep(5 * time.Second) // 何か実行
    finished <- 1               // goroutineを終了
    time.Sleep(1 * time.Second) // 後続の処理 
}


https://play.golang.org/p/AAh7t8TZMrf

こんな感じで、チャンネルを複数使用する祭に使用します。
selectは、caseに指定したチャネルのうち、どれかを受信するまで待機します。
待機したくない場合はcase default:をどのチャンネルも受信しなかった場合に呼ばれるため、待機することはありません。

近づく24時、なぜか拾わないガラスの靴

cinderella.go
<-magic.Broken
// シンデレラいそいで帰る
ball.Exit(cinderella)
// ガラスの靴を落としてしまう!!
falledShoes := cinderella.Shoes
cinderella.Shoes = nil

タイムリミットが近づいてきた(limitチャンネルに値が送信された)ため、魔法がとけてしまいます。
急いで帰るシンデレラですが、ガラスの靴を落としてしまったようです。

大丈夫、その靴王子が拾います

焦っていたためガラスの靴を拾わなかったシンデレラですが、
王子が拾ってくれたようです。

cinderella.go
// 王子, ガラスの靴を見つける
foundShoes := falledShoes

// 舞踏会終了
<-finished

実行結果

舞踏会開始
現在 19時
StepMother は踊っている
SisterA は踊っている
SisterB は踊っている
ella は踊っている
王子 は踊っている
現在 20時
StepMother は踊っている
SisterA は踊っている
SisterB は踊っている
ella は踊っている
王子 は踊っている
現在 21時
StepMother は踊っている
SisterA は踊っている
SisterB は踊っている
ella は踊っている
王子 は踊っている
現在 22時
StepMother は踊っている
SisterA は踊っている
SisterB は踊っている
ella は踊っている
王子 は踊っている
現在 23時
StepMother は踊っている
SisterA は踊っている
SisterB は踊っている
王子 は踊っている
0時が近づいている
魔法が解けそう!!
ella は舞踏会から抜け出し、帰宅した。
現在 24時
StepMother は踊っている
SisterA は踊っている
SisterB は踊っている
王子 は踊っている
現在 25時
StepMother は踊っている
SisterA は踊っている
SisterB は踊っている
王子 は踊っている
現在 26時
StepMother は踊っている
SisterA は踊っている
SisterB は踊っている
王子 は踊っている
舞踏会は終了

そして舞踏会も終了したようです。

靴を手がかりにシンデレラを捜す王子

王子は舞踏会であった女性を探すため、ガラスの靴のぴったり合う女性を部下たちに探させます。
合コンで連絡先を聞きそびれたため、SNSで見つけるみたいな感じだと思います。

cinderella.go
// 靴の持ち主を舞踏会の参加者の中から探している
for _, h := range ball.Entries {
    if h.Gender == Woman {
        if foundShoes.Wear(h) {
            fmt.Println("見つけた!")
        } else {
            fmt.Printf("%v: %vさんの靴ではない\n", prince.Name, h.Name)
        }
    }
}

しかし、ball.Entriesの中にはシンデレラはいないため見つかりません。

実行結果

王子: StepMotherさんの靴ではない
王子: SisterAさんの靴ではない
王子: SisterBさんの靴ではない

靴がぴったりシンデレラ

最後にシンデレラが靴を履くとぴったり。

cinderella.go
if foundShoes.Wear(cinderella) {
    fmt.Println("見つけた!")
}

実行結果

見つけた!

おしまい

そして二人は結婚したようです。
あとは省略。

あとがき

いかがでしたでしょうか。
書いていてなかなか面白かったです。

質問や、マサカリ大歓迎ですので、コメントor僕のTwitterまでお願い致します。