試行錯誤のための道具
(Common LispのREPL駆動開発)

  • Common LispのREPLは式を入力すると即座に結果を返す対話環境で、関数一つを数十マイクロ秒でコンパイルしながら試行錯誤できる。
  • ウォーターフォールは設計の不確実性に弱く、テスト駆動開発は個人開発には管理コストが重いと感じた経験から、REPL駆動開発にたどり着いた。
  • 答えが見えない状態からコードを書きながら設計を考える個人開発には、REPLによる探索的な開発スタイルが合っている。

関連記事

1. 大きなプログラムを書くには?

プログラミングを始めたころ、とにかくコードを書きながら進めていました。

大きなプログラムを書くには? ① 書きながら進める ② 規模が大きくなる ③ 把握できなくなる 小さいうちはOK 状態が散らばる 何かを変えると 別の箇所が壊れる 結局、一から作り直し → 正しい設計は最初からわからない 個人開発では「完成=次の機能追加」が始まる ウォーターフォールもTDDも、そのままでは重すぎた

動かしながら少しずつ機能を足していく。
最初のうちはそれでよかったのですが、規模が大きくなるにつれて頭では把握できなくなります。

あちこちに状態が散らばって、何かを変えると別のどこかが壊れる。
結局、それ以上先に進めなくなり、一から作り直し。

1.1. 要件定義やコード設計

何度かそういう経験をすると、どう設計したらよいのか興味も持ちました。
先にオブジェクトとプログラムの動作をしっかり設計して、それを順番に組み立てていく。

仕様や要件定義を元に、それを満たすコード設計を実装していく。
ウォーターフォール的なやり方は、正しく設計できれば、最もスムーズなはずです。

ただし、この「正しく設計できれば」が曲者。
プログラムを組み上げた段階で、コードの中の不具合が一度に一度に出てくる。
完成してから初めてバグに遭遇すると、どこを直すとどう動くのかが複雑に絡み合っていて、手がつけられなくなりました。

根本的な問題に気づきました。
企業でのソフトウェア開発と、個人で自分の作りたいプログラムを作る、では大きな違いがあります。
それは、完成すれば、すぐに次の機能が追加したくなること。
正しい設計は最初からわかりません。

1.2. テスト駆動開発

そこでテスト駆動開発を試しました。

部品ごとに「何が来たら何を返すか」をテストとして用意しておいて、それが通るように実装する。
部品ごとの動作確認が確実にできるので、バグの場所が絞れます。

これは、よかったのですが、テストを書くのが面倒なんですよね。
単純な作業の繰り返しで、つい後回しにしているうちに、結局最初と同じ問題に戻りました。

個人で開発するには、やや重い気がします。

1.3. 状態のない関数

ややこしさの原因は、オブジェクトの状態にあるか、とも考えました。

内部のデータは、プログラムを実行中にはどんどん変わり、いつの間にか想定外の状態になっています。
そこで、常に一定の結果を返す関数のほうが扱いやすいという流れで、関数型(?)にも書きやすいLispを使ってみることにしました。

2. Common LispのREPL駆動開発

Lispを使い始めると、関数ごとの独立性だけでなく、REPLによる開発がとても試行錯誤に合っていることがわかりました。

Common LispのREPL駆動開発 > (defun hello () “world”) HELLO > (hello) “world” > _ 1 関数を一つ書いて REPLで動かす 2 返ってきた値を見て 次の関数を書く 3 うまくいかなければ その場で書き直す 関数1つのコンパイル = 数十マイクロ秒 プロジェクト全体の再コンパイル不要

REPLはRead-Eval-Print Loopの略で、式を入力すると即座に評価して結果を返す対話的な環境です1

実際の開発はこんな感じです。

  • まず小さな関数を一つREPLで書いて動かす。
  • 返ってきた値を見て、次の関数を書く。
  • うまくいかなければその場で書き直す。

プロジェクト全体を再コンパイルする必要はなく、関数一つのコンパイルは数十マイクロ秒で終わります2

2.1. 試行錯誤の速度

REPLは確認のコストをほぼゼロにします。
この速さは単なる利便性ではありません。
思考のリズムが変わります。

「書いて、待って、確認する」ではなく「書いて、即見る」が繰り返されると、頭の中で次の問いが途切れない。
コンパイルを待つ数秒が、集中を切ります。

2.2. 変化の時代の設計手法

このような対話環境での柔軟な開発手法の良さは、ポール・グレアムの『ハッカーと画家』やケント・ベックの『SmallTalkベスト・プラクティス』でも知ることができます。

2000年代に高機能なGUIソフトウェアやウェブツールを作っていくときには、プログラムが複雑化し、緻密な設計にかかる時間や労力をどうクリアするか、という問題意識が生まれました。
ポール・グラハムが「スタートアップのような、何が正解かわからない状態で速く動く場面でLispが強い」と言っていたのも、この探索的な開発スタイルのことです3

3. チームとテスト

ただ、アジャイル開発やエクストリーム・プログラミングなどの考え方は主流になりましたが、REPL駆動開発は、ほとんど主流にはなっていません。
それは、職業としてプログラムを納品することと「コードで考える」ことの差にあると思います。

チーム開発と個人開発 ウォーターフォール 前提 要件・設計が 最初から明確 終わりがある開発 テスト駆動開発 前提 チームで役割分担 IFを先に決める 納期・スケール必要 REPL駆動開発 前提 答えが見えない状態 コードで考える 個人・探索的開発 優劣ではなく、前提の違い どの手法も、ある文脈のもとで設計されている 個人開発に他の文脈の手法をそのまま当てはめないこと

企業開発、チーム開発、個人開発で、それぞれ適したやり方は違うのです。

  • ウォーターフォールは、要件と設計が最初から見えていることを前提にします。
    ここまでできたら完成、という開発に終わりや区切りがある場合には有効です。
  • テスト駆動開発は、チームの役割分担のため、関数やオブジェクトの使い方(インターフェース)と期待する振る舞いを先に決めておくことを前提にしています。

ケント・ベックといえば、テスト駆動開発の提唱者でもあります。
これは、Smalltalkの対話的な環境での探索の感覚を、さらにチームで再現するための手法といえます4

職業としてのプログラミングでは、要件は外から与えられて、チームで動かして、納期がある。
スケーラビリティも必要です。

もちろん、テストにはコストもあります。
いったんインターフェースを決め、テストを作るとそれに縛られます。
チームワークにおいては、「朝令暮改」で変えるわけにはいかなくなるのです。

3.1. プロトタイプ

結局のところ、これは開発手法の優劣ではありません。

自分の場合は、答えがまだ見えていない状態からコードを書き始めています。
というより、コードを書きながら問題の理解を深め、何をどのように設計するのかを考えています。
頭の中にぼんやりある何かを、手を動かしながら形にしていく。

だから、REPL駆動開発が合うのかもしれません。
REPLは、コードとの「対話」です。
何が返ってくるかわからないまま触ってみて、その結果を見て次の問いを立てる。
試行錯誤そのものが開発の形になっています。

いろんな開発手法を文脈を理解せずに、そのまま当てはめてもしかたありません。

それにこれは、背反するものではありません。
対話的に設計してプロトタイプを作ってから、チーム開発に移行することも可能だからです。

  1. REPLという用語はLispの歴史に由来する。Read(読み取り)、Eval(評価)、Print(出力)、Loop(繰り返し)の頭文字で、1964年のPDP-1上のLisp実装で初めて使われた表現とされている。 – Read–eval–print loop – Wikipedia
  2. Emacs上でCommon Lispを操作するSLIME(Superior Lisp Interaction Mode for Emacs)を使うと、エディタで開いているソースコードの関数をC-c C-cのキー操作一つで実行中のLispプロセスにコンパイルして送り込める。プロジェクト全体のビルドを待たずに、関数単位でインクリメンタルに確認しながら開発を進められる。 – SLIME – Wikipedia
  3. ポール・グラハムは2001年のエッセイ「Beating the Averages」の中で、自身が共同創業したスタートアップViawebをLispで開発し競合に対して優位に立てたと述べた。Lispが素早い開発を可能にし、競合他社が真似できない機能を実装できたと論じている。 – Beating the Averages – paulgraham.com
  4. ケント・ベックはSmalltalk用のユニットテストフレームワークSUnitを開発した後、「テストを先に書いて後からコードを書く」という手法を定式化した。彼自身はこれを「発明」ではなく「再発見」と表現している。最初にxUnitフレームワークをSmalltalkで書いたとき、かつて読んだ本の記述を思い出してTDDを試したと語っている。 – Test-driven development – Wikipedia