Global Day of Coderetreat 2021 in #tddyyχの参加記

amixedcolor
436
5
2

Global Day of Coderetreat 2021 in #tddyyχの参加記

Published at November 15, 2021 9:43 p.m.
Edited at November 16, 2021 7:03 p.m.

はじめに

これはえいみえいみっくすことamixedcolorが、何の書き方も知らないままにつらつらと表題にあるイベントに参加してみた記録を書いたものです。時系列順に書きます。
注:大文字小文字の区別が雑です。

この記事を読むと得られると僕が思うものは以下の通りです。

  • ペアプロ・モブプロでTDDする時の交代のタイミングについて
  • ライフゲームの2通りの実装方針について
  • if文などの条件分岐文を使わずに条件分岐する方法
  • pythonのクラスでストリームのようにチェーンを実装することについて

目次

  1. 対面 : 全員オフラインでは初対面でどきどき、いろいろな説明を受けたり
  2. セッション1 : pythonのmapでライフゲーム
  3. セッション2 : bashでTDDしながらライフゲーム
  4. ランチ : 御アトラクタ様からのめちゃくちゃ美味しい、いいバーガー
  5. セッション3 : haskellでTDDしながらライフゲーム
  6. セッション4 : pythonのclassでTDDしながらライフゲームを完成させたかった
  7. おやつ : 御てやまぐ様からのいろんな美味しいお菓子
  8. セッション5 : pythonでTDDしながらif分岐禁止でライフゲーム
  9. セッション6 : 7, 8人でpythonでTDDとモブプロしながらライフゲーム
  10. 解散 : 楽しい雰囲気のままノベルティをもらいつつ片付けてお別れ

対面

受付終了時刻の5分くらい前にトコトコと入り、しばらく待機。時間になったらこの会の説明が行われました。内容として説明されたのは主に3つ。

  • tddyyχの名称について
    • tddyyxではない、最後の一文字はギリシャ文字のχ(chi/カイ)
    • tddをわいわいする会なのだと解釈、χに置き換えるの秀逸ですき
  • Global Day of Coderetreatについて
    • 世界で同日にCoderetreatをやること
Coderetreat(コードリトリート)とは、ソフトウェア設計と開発の基礎に焦点を当て、丸一日かけて集中的に練習することを目的としたプログラマのためのイベントです。"retreat"には「避難所」「保養所」といった意味合いがあり、「期限内に完成させなければならない」というプレッシャーから離れて純粋にプログラミングや設計を練習することに集中できます。 

このページの概要より

  • 振り返りについて
    • FUN/DONE/LEARNの形式で行う

自己紹介の時にenPiTが通じてちょっとびっくり。そしてmiholoveさんの僕のイメージは背が低くてキュートな感じだったようで、驚かれました(笑)。

セッション1

書き慣れたpythonで目でテストしながらライフゲームを書きます。右も左も分からない状態でしたが、とりあえず使い慣れていたvscodeのlive shareをつかいながらペアプロを5分交代で行いました。普通に書くのも味気ないので、mapをつかって書こうと頑張ってみました。いい線行きましたが残念ながら世界の時間をすすめられず…。mapを用いて書くというのは初めての試みでしたが光が見えました、楽しかったです。書いてて思い出しましたがこの時はライフゲームの隣接は隣接4マスだと思い込んで実装してましたね…。

session1.py
field = [[ 1 if j == 3 and 1 < i < 5  else 0 for i in range(10) ] for j in range(10) ]

d = [(0,1),(1,0),(0,-1),(-1,0)]

def update(li) :
    def calc(i, j) :
        print(i, j)
        s = sum([ li[li[i+p][j]][li[i][j+q]] for p,q in d if (0 < i < 9 and  0 < j < 9 ) ])
        ret = li[i][j] if s == 2 else 1 if s == 3 else 0
        return ret
    
    new_li = list(map(calc, range(10), range(10)))
    return new_li

for _ in range(15):
    field = update(field)

このセッションが終わったときのボードは以下の通り。まだ疎ながらも楽しい様子が伺えます。僕は水色の付箋に太いペンで書きました。

P_20211113_105443

セッション2

ペアを変えて次はなんとbashで挑戦。cyber-dojoというサービスを使って、お手軽にTDDをしながらやってみました。ペアの方はTDDには慣れているようで、とても助かったのを覚えています。初めの30分程度は配列を関数の返り値として渡せない問題に直面して大変でしたが、ペアの方が文字列でいいのではと提案してくれたおかげで、世界をハードコーディングだけど生成することに成功しました!実はまともにTDDをやるのは初めてだったのですが、少しだけその感覚を体験できた感じでした。初めての言語にトライするのもとっても楽しかったです!

hiker.sh
#!/bin/bash

answer()
{
  echo $((6 * 7))
}

create_board()
{
#  local -a board=()
  local board=("000111000")
  echo ${board}
}
test_hiker.sh
#!/bin/bash

source ./hiker.sh

<< HOGE
test_life_the_universe_and_everything()
{
   local expected=42
   local actual=$(answer)
   assert_equals ${expected} ${actual}
}
HOGE

test_board_size_is_correct()
{
local board=$(create_board)
# echo ${board}
local board_size=${#board}
assert_equals 9 ${board_size}
}

<< HOGE1
test_qiita()
{
declare -a array=()               # 宣言
declare -a array=("a" "b" "c")    # 初期化
echo ${#array[@]}              # 3
}
HOGE1

このセッションが終わったときのボードは以下の通り。いきなり結構埋まってきましたが、みんな調子が乗ってきたのでしょうか。僕は桃色の付箋にボールペンで書きました。

P_20211113_115512

ランチ

時間が経つのは早いものであっという間に2時間、2セッションが終わり、ランチタイムにmiholoveさんが楽しみにしといてとたくさん言っていたので期待に期待を重ねて食したハンバーガー。その期待すらも上回る美味しさ、肉厚感、ジューシーさとパンの香り、ソースの味、シャキシャキ、トロトロ。とにかく美味しかったです。御アトラクタ様ありがとうございます!

P_20211113_121805

セッション3

またペアを変えて今度はhaskellで挑戦。関数型言語は授業内でOCamlを少し触った程度で、その記憶もあやふやながら、サンプルコードをもとに文法を解読しながらTDD。このときにTDDでペアプロ・モブプロする時はテストでエラーを吐かせたら交代ということを知り、なるほどと思いながらそうやって交代しながらやりました。haskellの型の処理の仕方がちょっと不思議で、関数の返り値として帰ってくる時と、帰ってくる直前とでは型が違っていても(関数の返り値の型として宣言したものと違っていても)よしなにキャスト(?)をしてくれるみたいです(?)。知らない言語でも知っていることをやると大体の方針が定まっているので嬉しいですね。そういえば、このときライフゲームの実装方針も2つくらいあることを教えてもらいました。以下の2つで、僕が知っていて普段使っていたのは1つ目でした。

  1. 二次元配列を用意して、その中を走査して世代をすすめる
  2. 世界と、生きているマスの情報を別で保持して、生きているマスたちに含まれているかどうかなどで世代をすすめる

学びを得れるのがとても楽しくて楽しかったです(小並感)。

Hiker.hs
module Hiker where

answer :: Int
answer = 6 * 7

slice :: Int -> Int -> [a] -> [a]
slice from to xs = take (to - from + 1) (drop from xs)

world :: Int -> Int -> String
world x y = slice 0 (x * y + (y - 2)) (concatMap (++"\n") $ replicate y $ concat $ replicate x ".")
test_Hiker.hs
module Test_Hiker where

import Test.HUnit
import Hiker

life_the_universe_and_everything_test = TestCase (assertEqual "answer" (42) answer)
test_世界を作る_3かけ3 = TestCase (assertEqual "世界を作る3かけ3" ("...\n...\n...") (world 3 3))
test_世界を作る_5かけ4 = TestCase (assertEqual "世界を作る5かけ4" (".....\n.....\n.....\n.....") (world 5 4))
test_世界に生命を生む = TestCase (assertEqual "世界に生命を生む" ("...\n.*.\n...") (spawn (world 3 3) 1 1))

tests = TestList [life_the_universe_and_everything_test, test_世界を作る_3かけ3, test_世界を作る_5かけ4, test_世界に生命を生む]
main = do runTestTT tests

このセッションが終わったときのボードは以下の通り。右下のLearnが特に膨らんでいて、みんなDoneはできていないのがみてとれます。学びが多く、Doneを目指す会ではないからこその面白い形ですね。僕は桃色の付箋にボールペンで書きました。

P_20211113_135615

セッション4

またペアを変えて、今度は一回完成まで持っていきたい!!と願いながら、使い慣れたpythonで書き始めました。実は初めの方は関数で実装しようとしていましたが、それだといちいち世界を受け渡しする必要があったので面倒になり、クラスを使って実装する方針に変わりました。このピボットは良かったなあと、その時も、今も、感じています。

hiker.py
class World:
    world = list
    x = int
    y = int

    def __init__(self, x, y):
        self.world = [[0 for _ in range(x)] for _ in range(y)]
        self.x = x
        self.y = y
        
    def spawn(self, x, y):
        self.world[x][y] = 1
        return self
    
    def kill(self, x, y):
        self.world[x][y] = 0
        return self
    
    def next_gen(self):
        next_world = World(self.x, self.y)
        d = ((-1, -1),(-1, 0),(-1, 1),(0, -1),(0, 1),(1, -1),(1, 0),(1, 1))
        for i in range(len(self.world)):
            for j in range(len(self.world[0])):
                count = 0
                for p, q in d:
                    try :
                        if self.world[i+p][j+q] == 1 :
                            count += 1
                    except IndexError :
                        pass
                    
                print(i, j, count)
                if count == 3 and self.world[i][j] == 0:
                    next_world.spawn(i, j)
                elif (count == 2 or count == 3) and self.world[i][j] == 1:
                    next_world.spawn(i, j)
        self.world = next_world.world
test_hiker.py
from hiker import World
import unittest

class TestHiker(unittest.TestCase):

    def test_global_function(self):
        w = World(2, 3)
        self.assertEqual([[0, 0], [0, 0], [0, 0]], w.world)
        
    def test_spawn(self):
        w = World(2, 3)
        w.spawn(1, 1)
        self.assertEqual([[0, 0], [0, 1], [0, 0]], w.world)

    def test_kill(self):
        w = World(2, 3)
        w.spawn(1, 1).kill(1, 1)
        self.assertEqual([[0, 0], [0, 0], [0, 0]], w.world)
        
    def test_next_gen(self):
        w = World(3, 3)
        w.spawn(0, 0).spawn(0, 1).spawn(1, 0)
        w.next_gen()
        self.assertEqual([[1, 1, 0], [1, 1, 0], [0, 0, 0]], w.world)
        
    def test_next_gen2(self):
        w = World(3, 3)
        w.spawn(1, 0).spawn(1, 1).spawn(1, 2)
        self.assertEqual([[0, 0, 0], [1, 1, 1], [0, 0, 0]], w.world)
        w.next_gen()
        self.assertEqual([[0, 1, 0], [0, 1, 0], [0, 1, 0]], w.world)
        w.next_gen()
        self.assertEqual([[0, 0, 0], [1, 1, 1], [0, 0, 0]], w.world)

if __name__ == '__main__':
    unittest.main()  # pragma: no cover

さてこの時のボードは写真を撮り忘れてしまったので、公式ツイッターから拝借。

おやつ

またまた時間はあっという間で、おやつの時間になりました。お昼ご飯で結構お腹いっぱいかと思ったら、意外と入るもので、どら焼きなどを食べました。このどら焼きもまた美味しくて、もっちもっちのふわっふわでした。最高。御てやまぐ様ありがとうございます。

P_20211113_145242

セッション5

今度は紙に書いてあった楽しくするルールを一つ採用して、if分岐禁止でライフゲームを作ろうというふうになりました。こうなると楽しいのは世代を進めるところになるので、世代を進めるところに絞って実装をしました。少し残念なのがこの時のコードはないんですよね…。ロジック的には、if分岐の代わりに個数をカウントしてそれと対応する二進数に変換し、二進数のビット一つ一つが特定のパターンの時に次の世代の該当セルが生きているかどうかを出力するようにしました。ビット一つ一つの時も、ビット演算がifっぽいとのことだったので、ビット演算もせずに、1-xとxの掛け算で実装しました。そうかif分岐はこうすれば実装できるのか…という不思議な面白さがあってとても楽しかったです。この時ちょうど別のルールも偶然達成できていて、それは、各メソッドが4行以下ということでした。やったね!

この時のボードは以下の通り。たっくさん密になりました。少しずつDoneも増えてきているのは、Doneを細かく区切るようになってきたからだと個人的に思っています。僕は桃色の紙にボールペンで書きました。

P_20211113_162942

セッション6

最後は7, 8人のモブプロで、ライフゲームの実装方針は先に挙げた2つのうちの後者、世界と生きているマス(セル)を別でもつものをやりました。もちろんTDDでやりましたが、最後のセッションともなると僕もだいぶTDDに慣れてきました。このときえた1番の学びは、周囲9マスを見る時に、(-1, 0, 1)のタプルをもとに、permutations(tuple, 2)として、(0, 0)continueするというやり方でやればスマートになるというものでした。これで単位ベクトルを手で書く必要がなくなります。この記事を書いている時に思ったのが、周囲4マスの時でも、同じタプルをもとにしてあらかじめ作っておき、生成されたタプルの各要素の絶対値の和が1の時だけ使えば、四方向隣接の単位ベクトルが取れるなあということです。ちょっとだけスマート。こういう考え方は覚えておいて損はないと思うので覚えておきます。なお相変わらずコードはないです…。

earth.py
from itertools import permutations

class Earth:
    def __init__(self, n):
        self.n = n
        self.lives = set([])
    
    def to_string(self):
        result = ""
        
        for y in range(self.n):
            for x in range(self.n):
                if (x, y) in self.lives:
                    result += "*"
                else:
                    result += "."
            result += "\n"
            
        
        return result
    
    def spawn(self, x, y):
        self.lives.add((x, y))
        
    def tick(self):
        d = (-1, 0, 1)
        ts = permutations(d, 2)
        for y in range(self.n):
            for x in range(self.n):
                count = 0
                for p, q in ts:
                    if p == q == 0 : continue
                    if (x+p, y+q) in self.lives:
                        count += 1
                if count == 3:
                    self.lives.add((x, y))
                elif count == 2 and (x, y) in self.lives:
                    pass
                else :
                    self.lives.discard((x, y))
test_earth.py
from earth import Earth
import unittest


class TestEarth(unittest.TestCase):

    def test_地球は大きさ5の正方形(self):
        kotae = """.....
.....
.....
.....
.....
"""
        self.assertEqual(kotae, Earth(5).to_string())
        
    def test_地球は大きさ4の正方形(self):
        kotae = """....
....
....
....
"""
        self.assertEqual(kotae, Earth(4).to_string())
   
    def test_地球の2_1に生命を与える(self):
        kotae = """....
..*.
....
....
"""
        earth = Earth(4)
        earth.spawn(2, 1)
        self.assertEqual(kotae, earth.to_string())
        
    def test_地球で生命が生存する(self):
        kotae = """**..
**..
....
....
"""
        earth = Earth(4)
        earth.spawn(0, 0)
        earth.spawn(0, 1)
        earth.spawn(1, 0)
        earth.spawn(1, 1)
        earth.tick()
        self.assertEqual(kotae, earth.to_string())
        
    def test_地球で生命が生きたり死んだり(self):
        kotae = """....
***.
....
....
"""
        earth = Earth(4)
        earth.spawn(1, 0)
        earth.spawn(1, 1)
        earth.spawn(1, 2)
        earth.tick()
        self.assertEqual(kotae, earth.to_string())
        
if __name__ == '__main__':
    unittest.main()  # pragma: no cover

最終的なボードはこうなりました。いい感じに密です!!このうちの25枚くらいは僕が書いたものだと思います。今回も桃色の紙にボールペンで書きました。

P_20211113_173438

解散

あっというまでした。本当にあっという間。一人一言感想を言い合って、最後に御アトラクタ様からノベルティのDONEやDOING、TODOと書かれた磁石をいただきました。便利そう。まだ使えていないですが、大きめの磁石がくっつく場所を確保できたら使いたいですし、オフラインで開発する機会ができたらその時に持って行って使おうと思います。めちゃくちゃ余談ですが、会議室のしきりをレールに沿って戻す作業とても楽しいです。小さな夢が叶いました。幸せです。そんなこんなで片付けて、机も戻し、ゴミも捨て、解散でした。

おわりに

この記事を書いている時間もあっという間です。いつの間にか2時間以上経ってました。びっくり。一番言いたいのは、楽しい!また参加したい!ということです。コロナも下火ではあると僕は思っているので、次回開催を楽しみにしたいと思います。学びの数がとても多いです。あと、人と一緒に話しながらプログラミングすることの楽しさをあらためて感じられています。またTDDも体験できるのがとてもいいです。本当に学びの数が多い。知的好奇心が満たされていく音がひっきりなしでした。最後に、ここまで読んでくださった方、こんなに長い文を読んでくれてありがとうございました。何か読者の方にも得るものがあったなら、本望です。また書く時は書くと思うのでその時にお会いしましょう。では!

更新ログ

2021/11/16 19時ごろ : ゆーじさんから頂いたセッション6でのコードを追加しました。