LoginSignup
9
2

More than 3 years have passed since last update.

ミーティングをシミュレーションするGoのプログラムを日本語で書いた

Last updated at Posted at 2020-05-22

TL;DR

天から授かったアイデア 「ミーティングをシミュレーションせよ」

+

アホな自分 「日本語でGoを書いたらどうなるのかな」

合体 → なぜか、Goの文法を丁寧に説明する教育的な記事になった(気がする)

対象読者

  • プログラミング知識はそこそこあるが、Goを触ったことない人
  • Goの文法を知ってはいるけど、どう使うべきかわからない人

最終的な出力結果

悪いミーティングが開かれた場合

zawawa が 良い意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 良い意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 良い意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 良い意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!

良いミーティングが開かれた場合

hoge が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
hoge が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
hoge が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
hoge が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
hoge が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
zawawa が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
hoge が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
zawawa が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
zawawa が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!
zawawa が 良い意見 を基にした具体的な行動 を 2020-05-29 22:58:55.830343 +0900 JST までにする!

実装を解説していく

私が書いたコードを私が解説する記事がついに始まりました。

何事も最初は main() 関数から見ていくのが理解への近道です。

main関数

main.go
func main() {
    参加者 := []*メンバー{
        {ID: "zawawa", 名前: "働き者のざわ"},
        {ID: "hoge", 名前: "やる気ないほげ"},
    }
    次のミーティングの日時 := time.Now().AddDate(0, 0, 7)

    良いミーティング := 良いミーティングを作る(参加者, &次のミーティングの日時)
    ミーティングを行う(良いミーティング)

    悪いミーティング := 悪いミーティングを作る(参加者, &次のミーティングの日時)
    ミーティングを行う(悪いミーティング)
}

はい。未定義の型がたくさん出てきたので、細かく見ていきましょう。

main.go(main)
    参加者 := []*メンバー{
        {ID: "zawawa", 名前: "働き者のざわ"},
        {ID: "hoge", 名前: "やる気ないほげ"},
    }

この 参加者メンバー 構造体のポインタの配列です。
メンバー 構造体は、次のような定義です。

main.go
// メンバーID は、メンバーに与えられる固有のIDです.
type メンバーID string

// メンバー名 は、メンバーの名前を表す型です.
type メンバー名 string

// メンバー は、会議に参加したりしなかったりするメンバーです.
type メンバー struct {
    ID メンバーID
    名前 メンバー名

このように、 string であるような ID名前 のために、 メンバーIDメンバー名 の型を定義しています。
一見、冗長に見えますが、 string のままだと、何かしらの関数の引数で複数の string を渡す場合などで起こる他の変数との混在を、コンパイラレベルでエラーとして検知できるようになります。
見た目以上に便利なテクニックなので、ぜひ使ってみてください。

    参加者 := []*メンバー{
        {ID: "zawawa", 名前: "働き者のざわ"},
        {ID: "hoge", 名前: "やる気ないほげ"},
    }

こう書くことで、 参加者 という変数には、 ID = "zawawa"メンバー 型のインスタンスのポインタと、 ID = "hoge"メンバー 型のインスタンスのポインタの配列が格納されることになります。

そして、次の行がこちらです。

    次のミーティングの日時 := time.Now().AddDate(0, 0, 7)

Goの標準パッケージの一つである time パッケージで time.Now() 関数によって、現在時刻を表す time.Time インスタンスを生成し、それに7日足して、一週間後の time.Time 構造体のインスタンスを 次のミーティングの日時 変数に格納しています。

そして、実際に 良いミーティングを作る で得られる 良いミーティング インスタンスを ミーティングを行う 関数に渡しています。

    良いミーティング := 良いミーティングを作る(参加者, &次のミーティングの日時)
    ミーティングを行う(良いミーティング)

良いミーティング インスタンスは、 ミーティング インターフェイスの実装です。

どういうことやねん、となると思うので、 ミーティング インターフェイスを見てみます。

// ミーティング は、ミーティングを表すインターフェイスです. ここでは、ミーティングとは、次の行動を出力するものだけを指すこととします.
type ミーティング interface {
    意見を出し合う() []意見
    意見から次にやることを決める(出てきた意見 []意見) ([]*次にやること, error)
}

ミーティング インターフェイスは、先程出てきた 構造体 とは異なり、具体的な存在では有りません。 これこれの関数を実装した構造体を ミーティング インターフェイスとみなせるよ という宣言です。
ちなみに、ここで出てきた 意見次にやること は次のような定義です。

// 意見 は、ミーティング中に出てくる意見のことです. 次やることを決めるための材料になります.
type 意見 string

// 具体的な行動 は、「何を」するかを表現する型です.
type 具体的な行動 string

// 次にやること は、次にやることです. 誰がやるか、何をするか、いつまでにするかをしっかり決めましょう.
type 次にやること struct {
    誰が   メンバーID
    何を   具体的な行動
    いつまで *time.Time
}

ようするに、 ミーティング インターフェイスを満たすためには、次の2つの関数を持つ構造体を作って上げればよいことになります。

  • 意見 の配列を出力する 意見を出し合う() 関数
  • 意見 の配列を材料に、 次にやること の配列(とエラー)を出力する関数

ミーティング インターフェイスについてある程度眺めたところで、 ミーティングを行う 関数に戻ってみましょう。

ミーティングを行う 関数

func ミーティングを行う( ミーティング) {
    出し合った意見 := .意見を出し合う()
    次にやることリスト, エラー := .意見から次にやることを決める(出し合った意見)
    if エラー != nil {
        log.Fatalf("意見から次にやることを決めてるときに、エラーが起きました. エラー = %#v", エラー)
    }

    for _, 次にやること := range 次にやることリスト {
        fmt.Println(次にやること.文字列にする())
    }
}

なんだか、ごちゃごちゃしてるので、一行ずつ見ていきましょう。

    出し合った意見 := .意見を出し合う()

これによって、 ミーティング インターフェイスの 意見を出し合う 関数が呼び出され、 意見 の配列が出力されることになりますね。
そして、 出し合った意見 は、 ミーティング インターフェイスのもう一つの関数である 意見から次にやることを決める 関数に渡され、最終的に 次にやること の配列が返されます。

    次にやることリスト, エラー := .意見から次にやることを決める(出し合った意見)
    if エラー != nil {
        log.Fatalf("意見から次にやることを決めてるときに、エラーが起きました. エラー = %#v", エラー)
    }

ここで、 Golang独自のエラーハンドリングが現れましたね。 エラーerror インターフェイスを実装したインスタンスです。
error インターフェイスとは、Golangに組み込まれたインターフェイスで、 string を返す Error() 関数を持つものを言います。意外に単純ですね。

builtin.go
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

エラー そのものは、構造体へのポインタなので、「何も指し示さない」 nil という値も取ります。 エラー == nil のときは、 エラーが起きていない という意味になります。

    if エラー != nil {
         // 何かしらのエラーがあったってこと。
    }

ミーティングの末得られた 次にやることリスト にある各 次にやること 構造体を文字列にして、標準出力しているのがこちらです。

    for _, 次にやること := range 次にやることリスト {
        fmt.Println(次にやること.文字列にする())
    }

ミーティングを行う関数 がやっていることまとめ

ミーティング インターフェイス(を実装した構造体のポインタ)を受け取り、 意見を出し合う 関数を実行して 意見 を出し合い、 意見から次にやることを決める 関数をでそれらを 次にやること に変換するという流れになります。

ミーティング インターフェイスはなぜ必要なのか?

なんで、 ミーティング インターフェイスなんてものを宣言したのでしょうか?
私は、ミーティングをシミュレートするコードを書きたいと考えたときに、まず、 「ミーティングとは何か?」 を考えました。
そのときに思い浮かぶものは、 ミーティングがどういう流れで行われ、どういう機能があるかということです。

大概のミーティングでは、最初にアジェンダ的なものがあり、そこから誰かがファシリをしながら、全体で議論をしていき、最終的にネクストアクション(ここでは 次にやることリスト ) にたどり着くはずです。

そのような、 一般的なミーティングで行われる行動を表すのに用いるのが インターフェイス です

個別のミーティングがどういう中身で、どういう結論を導くかという具体的な部分を考えるのではなく、 ミーティングが持つ 抽象的な性質 にフォーカスしているのです。

(もちろん、このインターフェイス自体もミーティングの一側面を表しているに過ぎないのでご了承を)

ミーティング インターフェイスの実装を一つ見てみる( 悪いミーティング

// 悪いミーティング は、目指してはいけない悪いミーティングです.
type 悪いミーティング struct {
    参加者         []*メンバー
    次のミーティングの日時 *time.Time
}

func 悪いミーティングを作る(参加者 []*メンバー, 次のミーティングの日時 *time.Time) ミーティング {
    return &悪いミーティング{
        参加者:         参加者,
        次のミーティングの日時: 次のミーティングの日時,
    }
}

func ( *悪いミーティング) 意見を出し合う() []意見 {
    var 発散しがちな議論 []意見

    // 時間だけは伸びる.
    for i := 0; i < 20; i++ {
        if i%5 == 0 {
            発散しがちな議論 = append(発散しがちな議論, 意見("良い意見"))
            continue
        }
        発散しがちな議論 = append(発散しがちな議論, 意見("見当外れの意見"))
    }
    return 発散しがちな議論
}

func ( *悪いミーティング) 意見から次にやることを決める(出てきた意見 []意見) ([]*次にやること, error) {
    // 出てきた意見の吟味は特にしない

    var 次にやることリスト []*次にやること
    for _, 意見の一つ := range 出てきた意見 {
        やること := &次にやること{
            // 誰かの負担が大きい
            誰が: .参加者[0].ID,
            何を: 意見をやることに変換する(意見の一つ),
            // 特にいつまでと決めない
            いつまで: nil,
        }
        次にやることリスト = append(次にやることリスト, やること)
    }
    return 次にやることリスト, nil
}

詳細な説明は省きますが、注目してほしいのは次の点です。

悪いミーティングを作る がコンストラクタの役割を果たし、 悪いミーティング 構造体のポインタを ミーティング インターフェイスとして返す

func 悪いミーティングを作る(参加者 []*メンバー, 次のミーティングの日時 *time.Time) ミーティング {
    return &悪いミーティング{
        参加者:         参加者,
        次のミーティングの日時: 次のミーティングの日時,
    }
}

いくつか、 悪いミーティング 構造体にセットするための引数が与えられていますが、特に重要なのは、 ミーティング インターフェイスが戻り値となっている点です。
しかし、関数の中では &悪いミーティング{} という構造体のポインタが返却されています。

ここで 悪いミーティング 構造体のポインタから ミーティング インターフェイスへの変換を行うためには、 悪いミーティング 構造体が ミーティング インターフェイスが持つ関数を実装していなければなりません。

Javaなどの言語では、Interfaceをclassの定義で指定して、明示的に実装を行いますが、Golangでは、そのような明示的に構造体がインターフェイスを実装する文法はありません。

このようなコンストラクタを書いてあげると、コンパイラが暗黙的に構造体がインターフェイスを実装しているかを確かめてくれるのです。

悪いミーティング を開催してみた

さて、ようやく実装できたので、実行してみます。

main.go
func main() {
    参加者 := []*メンバー{
        {ID: "zawawa", 名前: "働き者のざわ"},
        {ID: "hoge", 名前: "やる気ないほげ"},
    }
    次のミーティングの日時 := time.Now().AddDate(0, 0, 7)

    悪いミーティング := 悪いミーティングを作る(参加者, &次のミーティングの日時)
    ミーティングを行う(悪いミーティング)
}

結果: 悪いミーティングをすると、誰か一人が大量の見当外れの意見を基にしたネクストアクションをさせられることに

非常に恐ろしいシミュレーション結果になってしまいましたね。みなさんのミーティングはこうならないことを祈ります。

zawawa が 良い意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 良い意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 良い意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 良い意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!
zawawa が 見当外れの意見 を基にした具体的な行動 をする!

まとめ

思いつきで書き始めた割になかなかの分量になってしまうのが私の癖ですが、いかがでしたでしょうか。

特に、 悪いミーティング は具体的な何かを指すものではなく、仮想上のミーティングなので、関係者よろしくお願いします。

Qiitaの自分のページ( @zawawahoge )を見ると、

image.png

のように、業務で使ってる Golangの記事ばかりをLGTMしてる最近の私ですが、元々はPython大好き少年でした。
投稿した記事もPythonのものが多かったですが、そろそろGolangもいい感じに使えるようになってきた(気がするだけかもしれない)ので、そろそろ記事にしたいなーと考えていたところでした。

実は、今回のこの記事は、自分なりにここ半年ほどで実務経験で学んだテクニックなどが詰まっていて、ひそかに書いてよかったと思っています。

最初の発端はともかく、この記事を読んで、 「Goって面白そうだな」と感じて、触ってみてくれる人がいると、とても嬉しいです

長々と読んでくださりありがとうございました。

面白いと思ってもらえたら、LGTMよろしくお願いします!

Twitter( @zawawahoge ) もやってますので、ぜひぜひご感想などいただければ幸いです。

9
2
0

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
9
2