Skip to content

Instantly share code, notes, and snippets.

@smeghead
Last active September 12, 2024 11:02
Show Gist options
  • Select an option

  • Save smeghead/65371a41d50abc3ae42b69ab9ac8e69c to your computer and use it in GitHub Desktop.

Select an option

Save smeghead/65371a41d50abc3ae42b69ab9ac8e69c to your computer and use it in GitHub Desktop.
phel-lang で小さいプログラムを作る方法 (TDD)

phel-lang で小さいプログラムを作る方法 (TDD)

phel-lang を使って小さいプログラムを作るのをTDD(テスト駆動開発)で進める場合に、どのように作成しているかを順番に書いてみます。

作るものを決める

一次元セル・オートマトンでフラクタル図形を描画することができるというのがおもしろいと思い、phel-langで実装してみることにしました。

1次元セル・オートマトン | Wikipedia

世代が変わる毎に、隣接するセルの状態により次の状態が決まる、ということはわかったのですが、それが画像として出力されることが最初は理解できなかったです。

  • 第一世代は、真ん中に 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)を実行しテストがパスすることを確認します。

ruleというネームスペースで構造体と次世代の値を決定する関数を定義することにした

セルの次の世代の値を決定するルールは以下のようになります。

  • 現世代の、前のセルの値とセルの値と後のセルの値を連結したものを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に切り出しました。

最後のピースであるセル配列の表示用文字列作成関数を作る

セル配列に格納されているデータは、01という値なので、画面表示用の文字列に変換する必要があります。 テストを追加します。

  • 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

image

ボトムアップで作ってきた部品を使って、一次元セル・オートマトンを表示するプログラムが完成しました。実際には、細かいリファクタリングや試行錯誤をしていますが、概ねこのような順序で作成しました。

phel-lang で小さいプログラムを作るのは、意外と簡単かも?と思ったら、試してみるのはどうでしょう?

phel-lang を始める方法

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment