phel-lang を使って小さいプログラムを作るのをTDD(テスト駆動開発)で進める場合に、どのように作成しているかを順番に書いてみます。
一次元セル・オートマトンでフラクタル図形を描画することができるというのがおもしろいと思い、phel-langで実装してみることにしました。
世代が変わる毎に、隣接するセルの状態により次の状態が決まる、ということはわかったのですが、それが画像として出力されることが最初は理解できなかったです。
- 第一世代は、真ん中に
1のセルがあり、他のセルは0という状態からスタートする。 - 第一世代を1行目に記録して、次の世代を2行目に記録のように、上から下へ向かって世代を記録した結果が出力された画像になる。
これがわかったので、作り方も見えてきました。 セル配列が存在していて、次世代の状態を求めながら、世代を出力していけば絵を出力することができそう。
まずは1つの世代を表現するセル配列を、Line という構造体として使えるようにします。
最初にテストを追加します。
- tests/modules/line-test.phel
(ns cellular-automaton\modules\line-test
(:require cellular-automaton\modules\line)
(:require phel\test :refer [deftest is]))
(deftest test-line-initial
(let [l (line/create-line 80)]
(is (= 0 (get-in l [:cells 0])))))lineというネームスペースのcreate-line という関数を使って構造体Lineの値を取得して、セル配列の1番目の要素の値が0であることを確かめるテストを追加します。
ここで、vender/bin/phel test を実行してテストが失敗することを確認しておきます。
- src/modules/line.phel
(ns cellular-automaton\modules\line)
(defstruct Line [cells])
(defn create-line [width]
(Line (values (php/array_fill 0 width 0))))構造体Lineと関数create-lineを定義し、再度テスト(vender/bin/phel test)を実行しテストがパスすることを確認します。
セルの次の世代の値を決定するルールは以下のようになります。
- 現世代の、前のセルの値とセルの値と後のセルの値を連結したものを2進数として表現すると、2^3=8種類の状態が存在することになる。
- それぞれの状態の場合に、次世代のセルの状態がどうなるかを示すと、8個の0/1の値になり、2^8=256種類のルールが存在することになる。
ruleというネームスペースでnext-valueという関数を提供すれば良さそうなので、テストコードを追加します。
- tests/modules/rule-test.phel
(ns cellular-automaton\test\modules\rule-test
(:require cellular-automaton\modules\rule)
(:require phel\test :refer [deftest is]))
(deftest test-rule
(let [r (rule/Rule 30)]
(is (= 0 (rule/next-value r 0)))
(is (= 1 (rule/next-value r 1)))
(is (= 1 (rule/next-value r 2)))
(is (= 1 (rule/next-value r 3)))
(is (= 1 (rule/next-value r 4)))
(is (= 0 (rule/next-value r 5)))
(is (= 0 (rule/next-value r 6)))
(is (= 0 (rule/next-value r 7)))))ここで、vender/bin/phel test を実行してテストが失敗することを確認しておきます。
Ruleという構造体があり、next-valueという関数に構造体と現世代の該当のセルの状態を表す数値を指定すると、次世代の値が返却されます。
(ns cellular-automaton\modules\rule)
(defstruct Rule [rule])
(defn next-value [r v]
(if (bit-test (r :rule) v)
1
0))構造体Ruleと関数next-valueを定義し、再度テスト(vender/bin/phel test)を実行しテストがパスすることを確認します。
lineネームスペースに、next-lineという次世代のセル配列を取得する関数を追加します。
合わせて、初期のセル配列を作成するために、中央のセルだけ1を設定する関数setup-centerも作成します。
まずはテストを作成します。
- tests/modules/line-test.phel
(ns cellular-automaton\test\modules\line-test
(:require cellular-automaton\modules\line)
(:require cellular-automaton\modules\rule)
(:require phel\test :refer [deftest is]))
(deftest test-setup-center-bit
(let [l (line/create-line 80)
l (line/setup-center l)]
(println l)
(is (= 1 (get-in l [:cells 40])))))
(deftest test-next-line
(let [l (line/create-line 80)
l (line/setup-center l)
r (rule/Rule 30)
l (line/next-line l r)]
(println l)
(is (= 1 (get-in l [:cells 39])))
(is (= 1 (get-in l [:cells 40])))
(is (= 1 (get-in l [:cells 41])))))ここで、vender/bin/phel test を実行してテストが失敗することを確認しておきます。
- src/modules/line.phel
(defn setup-center [l]
(Line (put (l :cells) (php/intval (/ (count (l :cells)) 2)) 1)))
(defn- current-value [l index]
(php/bindec
(format
"%d%d%d"
(or (get-in l [:cells (dec index)]) "0")
(get-in l [:cells index])
(or (get-in l [:cells (inc index)]) "0"))))
(defn next-line [l r]
(Line (map-indexed |(rule/next-value r (current-value l $1))
(l :cells))))関数next-lineを定義し、再度テスト(vender/bin/phel test)を実行しテストがパスすることを確認します。
next-line関数から、ちょっと煩雑な処理をしている現在の値を調べる処理をcurrent-valueに切り出しました。
セル配列に格納されているデータは、0か1という値なので、画面表示用の文字列に変換する必要があります。
テストを追加します。
- tests/modules/line-test.phel
(deftest test-line-expression
(let [l (line/create-line 80)
l (line/setup-center l)
r (rule/Rule 30)
l (line/next-line l r)
expected " ### "]
(is (= 80 (php/mb_strlen (line/expression l))))
(is (= expected (line/expression l)))))実装を追加します。文字列連携の関数を使うために、ネームスペースの定義に(:require phel\str)を追加しています。
- src/modules/line.phel
(ns cellular-automaton\modules\line
(:require phel\str)
(:require cellular-automaton\modules\rule))
(defn expression [l]
(str/join "" (map |(if (= $ 1) "#" " ") (l :cells))))再度テスト(vender/bin/phel test)を実行しテストがパスすることを確認します。
必要な部品は揃ったので、エントリポイントとなる src/main.phel を作成します。
(ns cellular-automaton\main
(:require cellular-automaton\modules\line)
(:require cellular-automaton\modules\rule))
(when-not *build-mode*
(def width 80)
(def height 40)
(def rule-seed 90)
(def type :center) # :center or :random
(loop [l (apply (case type
:center line/setup-center
:random line/setup-random) [(line/create-line width)])
r (rule/Rule rule-seed)
count 0]
(println (line/expression l))
(if (> count height)
nil
(recur (line/next-line l r) r (inc count)))))vendor/bin/phel run src/main.phelボトムアップで作ってきた部品を使って、一次元セル・オートマトンを表示するプログラムが完成しました。実際には、細かいリファクタリングや試行錯誤をしていますが、概ねこのような順序で作成しました。
phel-lang で小さいプログラムを作るのは、意外と簡単かも?と思ったら、試してみるのはどうでしょう?
