はじめに
アドベントカレンダーでクリスマスの日に順番が回ってきたので、クリスマスツリーを描きます🌲🎅
こんにちは、株式会社ヤプリ サーバーサイドエンジニアの坂本(@mohvuba)です。
なぜクリスマスツリー?
ヤプリでは月に1回程度、Go界隈で有名なtenntennさんをお招きしてGoの勉強会を開催しています。 その中でここ最近、ツリー構造のオブジェクトの二分木探索をイテレータを用いて行うという内容のものがありました。
冒頭で書いたように、私がアドベントカレンダーで25日の担当になり、 12/25といえばクリスマス、クリスマスといえばクリスマスツリー! ということで、タイトル通りの記事を書くに至りました。
クリスマスツリーを描く?そんなネタ、出尽くしてるんじゃないの?と思われるかもしれません。
君のような勘のいいガキは嫌いだよ
ご明察のとおり、クリスマスツリーの描画自体はすでに行われている方の記事(Goでクリスマスツリーを表示する)を見つけることができました。 そこで今回は、その記事およびリポジトリの内容を参考にしつつ、イテレータを取り込むという点に特化したいと思います。*1
いざ、描画
ベースとなるコード
下記のリポジトリのコードを参考にします。*2
ブログ記事もこちらのリポジトリとともに最後に参考文献として貼っておきます。
今回はこのxmastreeに対して
- 描画をイテレータに変更する
- 飾りのパターンを増やし、各飾りがいくつあるかを二分木探索+イテレータで集計する
という二つのことを行いたいと思います。
そのために、以下のような手を加えます。
- struct
Tree
を改良する - オーナメントなど、他のものも出してみる
- 描画する箇所でイテレータを使う
- 飾りの各数をイテレータで集計した値を一緒に出してみる
Tree structを改良する & オーナメントなど、他のものも出してみる
イテレータ化、および絵文字の導入にあたってstructをもう少し細分化します。
- 元の
Tree
はどちらかというと描画のためのメタ的な設定値と言えるので、TreeSetting
として残します Leaf
を定義します。Leaf
はオブジェクト(string)を持たせるようにし、葉または飾りの絵文字を持ちますBranch
を定義します。Branch
は複数枚のLeaf([]*Leaf)
を持ちます- 先ほどの
TreeSetting
と[]*Branch
もつTree
を定義します
上記に則ったtree.goのソースコードがこちらです
【tree.go】
package tree import ( "fmt" "iter" "math/rand" "time" ) type ( ChristmasDecoration string TreeSetting struct { MaxWidth int LeafHeight int StemWidth int StemHeight int TotalHeight int } Leaf struct { Object ChristmasDecoration } Branch struct { Leaves []*Leaf } Tree struct { *TreeSetting Branches []*Branch } ) const ( Default ChristmasDecoration = "¥" Bell ChristmasDecoration = "🔔" Star ChristmasDecoration = "⭐" Candy ChristmasDecoration = "🍬" Snow ChristmasDecoration = "⛄️" Present ChristmasDecoration = "🎁" Sox ChristmasDecoration = "🧦" ) var leafObjectPattarn = []ChristmasDecoration{Default, Default, Default, Default, Default, Default, Default, Default, Default, Default, Default, Default, Bell, Star, Candy, Snow, Present, Sox} func NewBranch() *Branch { return &Branch{} } func NewLeaf() *Leaf { return &Leaf{ Object: getLeafObject(), } } func NewTree( branchHeight int, stemWidth int, stemHeight int, totalHeight int, ) *Tree { branches := make([]*Branch, branchHeight) for i := 0; i < len(branches); i++ { branches[i] = NewBranch() } return &Tree{ TreeSetting: &TreeSetting{ MaxWidth: branchHeight*2 + 1, LeafHeight: branchHeight, StemWidth: stemWidth, StemHeight: stemHeight, TotalHeight: totalHeight, }, Branches: branches, } } func (b *Branch) SetLeaves(amount int) { for i := 0; i < amount; i++ { b.Leaves = append(b.Leaves, NewLeaf()) } } func echoString(str string, amount int) []string { strs := make([]string, 0, amount) for i := 0; i < amount; i++ { strs = append(strs, str) } return strs } func getLeafObject() ChristmasDecoration { // ランダムにオブジェクトを与える rand := rand.New(rand.NewSource(time.Now().UnixNano())) i := rand.Intn(len(leafObjectPattarn)) return leafObjectPattarn[i] }
描画する箇所でイテレータを使う
先ほどのstructの改変ができたら描画していきます。
描画のため、Tree
に以下のメソッドを実装します
- 設定値を元にイテレータを定義して返す
Set()(iter.Seq[ChristmasDecoration], iter.Seq[string])
- 葉と幹の描画用のイテレータをそれぞれ受け取って葉と幹を一段ずつ描画する
DrawLine(iter.Seq[ChristmasDecoration], iter.Seq[string])
【tree.go】
// Set 木の葉, 幹のを描画するためのイテレータを返す func (t *Tree) Set() (iter.Seq[ChristmasDecoration], iter.Seq[string]) { return func(yield func(ChristmasDecoration) bool) { // 葉 for i, branch := range t.Branches { leafAmount := i*2 + 1 - (i/5)*4 branch.SetLeaves(leafAmount) // 葉の数を設定できるこのタイミングで葉にオブジェクトを設定する spaceLength := (t.MaxWidth - leafAmount) / 2 strs := make([]string, 0, spaceLength*2+leafAmount+1) strs = append(strs, echoString(" ", spaceLength)...) for _, leaf := range branch.Leaves { strs = append(strs, string(leaf.Object)) } strs = append(strs, echoString(" ", spaceLength)...) strs = append(strs, "") var str string for _, s := range strs { str += s } if ok := yield(ChristmasDecoration(str)); !ok { return } } }, func(yield func(string) bool) { // 幹 spaceLength := (t.MaxWidth-t.StemWidth-2)/2 + 3 for i := 0; i < t.StemHeight; i++ { strs := make([]string, 0, spaceLength*2+t.StemWidth+3) strs = append(strs, echoString(" ", spaceLength)...) strs = append(strs, echoString("[", 1)...) strs = append(strs, echoString(" ", t.StemWidth)...) strs = append(strs, echoString("]", 1)...) strs = append(strs, echoString(" ", spaceLength)...) strs = append(strs, "") var str string for _, s := range strs { str += s } if ok := yield(str); !ok { return } } strs := make([]string, 0, spaceLength*2+t.StemWidth+3) strs = append(strs, echoString(" ", spaceLength)...) strs = append(strs, echoString("`", t.StemWidth+2)...) strs = append(strs, echoString(" ", spaceLength)...) strs = append(strs, "") var str string for _, s := range strs { str += s } if ok := yield(str); !ok { return } if ok := yield("\r\n"); !ok { return } if ok := yield("\r\n"); !ok { return } } } // DrawLine 葉,幹を表示する func (t *Tree) DrawLine(leaves iter.Seq[ChristmasDecoration], stem iter.Seq[string]) { fmt.Print("\033[32m") for s := range leaves { fmt.Println(s) } fmt.Print("\033[31m") for s := range stem { fmt.Println(s) } }
元のコードが一段ごとに葉の枚数を後から決定し描画する仕様になっていたため、それに倣う形でBranch
がどのようなLeaf
を持つかはBranch
ごとの描画処理の際に決定されるようになっています。
*3
では、main.goからこれらを呼び出してみます。
【main.go】 *4
package main import ( "advent-calendar/tree" "flag" "fmt" "iter" "os" "time" ) func main() { var ( size = flag.Int("size", 8, "size") speed = flag.Int("speed", 100, "speed") ) flag.Usage = usage flag.Parse() for s := range display(*size, *speed) { fmt.Print(s) } } func usage() { usage := "" usage += "Usage: xmastree: [OPTION]\n" usage += " -size size\n" usage += " size of the tree. Default is 8. \n" usage += " -speed speed\n" usage += " speed of display tree. Default is 100. \n" _, err := fmt.Fprintf(os.Stderr, usage) if err != nil { fmt.Println(err) os.Exit(2) } } func display(size, speed int) iter.Seq[string] { return func(yield func(string) bool) { maxHeight := size*2 + 9 for i := 0; i < size; i++ { if i != 0 { if ok := yield(fmt.Sprintf("\033[%vA", maxHeight)); !ok { return } } tree := tree.NewTree(i*2+5, i+1, 2, maxHeight) leaf, stem := tree.Set() tree.DrawLine(leaf, stem) time.Sleep(time.Duration(speed) * time.Millisecond) } fmt.Print("\033[1A") } }
結果
絵文字のサイズ問題やターミナルの環境依存によってやや崩れはありますが、ちゃんとクリスマスツリーですね!
飾りの各数をイテレータで集計した値を一緒に出してみる
一緒に出すという点についてですが、
Branch
を処理するタイミングでしか葉の中身や枚数を決定できないという事情から、ツリーを描画しないとそもそも飾りの集計ができないためそのように対応します。
二分木探索を行うために、Tree
がもつ線形の[]*Branch
を親子構造に変換するところから始めます。
そのために、Tree
にメソッドMakeBranchBTree() error
を追加で実装します。(木なのか枝なのかややこしい...)
【tree.go】
// MakeBranchBTree Branchの木構造を作成する func (t *Tree) MakeBranchBTree() error { if len(t.Branches) == 0 { return nil } for i := 0; i < len(t.Branches); i++ { if len(t.Branches[i].Children) > 0 { return errors.New("すでに子ノードが存在します") } leftIndex := 2*i + 1 rightIndex := leftIndex + 1 // 右の子ノードを左の子ノードから計算 // 両方のインデックスが範囲外なら終了 if leftIndex >= len(t.Branches) && rightIndex >= len(t.Branches) { break } // 左の子ノードが範囲内の場合に追加 if leftIndex < len(t.Branches) { t.Branches[i].Children = append(t.Branches[i].Children, t.Branches[leftIndex]) } // 右の子ノードが範囲内の場合に追加 if rightIndex < len(t.Branches) { t.Branches[i].Children = append(t.Branches[i].Children, t.Branches[rightIndex]) } } return nil }
続いて、[]*Branch
を受け取って二分木探索をするイテレータを返す関数BranchBSearchIter(*Branch)iter.Seq[string]
を作成します。
絵文字より文字列の方が集計しやすそうなので、絵文字と文字列をmap
を用いて変換したものをyield()
します。
【tree.go】
const ( DefaultKey = "default" BellKey = "bell" StarKey = "star" CandyKey = "candy" SnowKey = "snow" PresentKey = "present" SoxKey = "sox" ) var decorationMap = map[ChristmasDecoration]string{ Default: DefaultKey, Bell: BellKey, Star: StarKey, Candy: CandyKey, Snow: SnowKey, Present: PresentKey, Sox: SoxKey, } func BSearchBranchIter(b *Branch) iter.Seq[string] { return func(yield func(string) bool) { for _, child := range b.Children { for _, leaf := range child.Leaves { if ok := yield(decorationMap[leaf.Object]); !ok { return } } for s := range BSearchBranchIter(child) { if ok := yield(s); !ok { return } } } } }
再帰的に自身を呼び出してその戻り値であるイテレータを回すのがポイントですね。 イテレータの関数定義の中でそのイテレータを返している関数自体を呼び出してその戻り値のイテレータを回している感じです。*5
main.goで呼び出してみます。
【main.go】
package main import ( "advent-calendar/tree" "errors" "flag" "fmt" "iter" "os" "time" ) func main() { var ( size = flag.Int("size", 8, "size") speed = flag.Int("speed", 100, "speed") ) flag.Usage = usage flag.Parse() for s := range display(*size, *speed) { fmt.Print(s) } for k, v := range countMap { fmt.Printf("\n%s: %d\n", k, v) } } func usage() { usage := "" usage += "Usage: xmastree: [OPTION]\n" usage += " -size size\n" usage += " size of the tree. Default is 8. \n" usage += " -speed speed\n" usage += " speed of display tree. Default is 100. \n" _, err := fmt.Fprintf(os.Stderr, usage) if err != nil { fmt.Println(err) os.Exit(2) } } func display(size, speed int) iter.Seq[string] { return func(yield func(string) bool) { maxHeight := size*2 + 9 for i := 0; i < size; i++ { if i != 0 { if ok := yield(fmt.Sprintf("\033[%vA", maxHeight)); !ok { return } } tree := tree.NewTree(i*2+5, i+1, 2, maxHeight) leaf, stem := tree.Set() tree.DrawLine(leaf, stem) // 飾りの数を数える(最後のツリーのみ) if i == size-1 { if err := aggregate(tree); err != nil { yield(err.Error()) return } } time.Sleep(time.Duration(speed) * time.Millisecond) } fmt.Print("\033[1A") } } var countMap = map[string]int{ tree.DefaultKey: 0, tree.BellKey: 0, tree.CandyKey: 0, tree.StarKey: 0, tree.SnowKey: 0, tree.PresentKey: 0, tree.SoxKey: 0, } func aggregate(t *tree.Tree) error { if err := t.MakeBranchBTree(); err != nil { return err } if len(t.Branches) == 0 { return errors.New("Branchが存在しません") } for s := range tree.BSearchBranchIter(t.Branches[0]) { switch s { case tree.DefaultKey: countMap[tree.DefaultKey]++ case tree.BellKey: countMap[tree.BellKey]++ case tree.CandyKey: countMap[tree.CandyKey]++ case tree.StarKey: countMap[tree.StarKey]++ case tree.SnowKey: countMap[tree.SnowKey]++ case tree.PresentKey: countMap[tree.PresentKey]++ case tree.SoxKey: countMap[tree.SoxKey]++ } } return nil }
main.goで扱われるのは[]*Branch
を持ったTree
型のインスタンスですが、親子構造を持つため集計に使うのは[]*Branch
の中の先頭の要素だけである点に注意してください。*6
結果
最後に
というわけで今回はイテレータと再帰処理を使ってクリスマスツリーを出力してみました! このような遊び心のあるプログラミングを通してGoの新しい機能が学べる機会があるのはすごくいいですね。
ヤプリでは現在エンジニア採用に力を入れております。 本記事についてや、ヤプリについて具体的なお話を聞いてみたいと思っていただいていた場合は、ぜひカジュアル面談にご応募いただければと思います! 一緒にGoStudyなどを通じて高め合いましょう!
参考記事・リポジトリ
*1:ゼロから描画プログラムを書こうとして、やれピラミッド状を描くなら平方数のタイミングで改行せねばだの、中央に配置するために空白ブロックを用意しておくのかだの細かく考えて時間を浪費していたのは内緒
*2:リポジトリの制作者に連絡し、使用許可をいただいております。
*3:もっと綺麗なやり方はあるかもしれないですが、目的はイテレータを使用することなので、ご容赦ください🙇
*4:ツリーの葉の文字を「¥」以外の絵文字も適用するために必要な改変と、イテレータへの改変以外は行っていませんが、一部改変を行っていない箇所などを便宜上コード欄に一緒に載せています。
*5:何を言ってるかわからないw
*6:このあたりは再帰処理を紹介するために無理やり親子構造を作ったことに起因していて、特殊な感じになってしまっていることをお詫び申し上げます🙇