見出し画像

Retro-gaming and so on

テスト駆動開発

実は以前から「プログラミングを今からはじめてみたい」と言う人に本当に薦めたいプログラミング言語はOCamlだった。
理由はいつぞや書いたが、プログラミング言語そのものは、モダンで高機能であれば確かにいいが、そこはそれほど重要じゃない。より重要なのは、「良いプログラミング入門書があるかどうか」なんだ(※1)。
そして、今、市場にある本で、本当に「プログラミングを今まで全くやった事がない」人に、個人的に勧められる本はこれしかない、って思っている。

 
取り敢えずこれさえあれば第一歩として何とかなる、と言う本だ。
ただ、本音は本音として、何故にじゃあこれを勧めないのか、と言うのは、これで扱ってるプログラミング言語、OCamlにはWindows版がないからだ。
OCamlはLinuxや、同じUNIX系OSを搭載しているiMacでしか基本使えない。そんな言語を使ってる本はいくら良くても薦められない(笑)。だから断腸の思いで今まで一回もこの本を薦めてないのでR(死語)。
OCamlをやる為だけにiMacを買え、とかあるいはLinuxをインストールしろ、とかWSLを入れろ、とかCygwinを入れろ、とか、そうなるとメチャクチャなんだよ。以前、初心者にIDEなんざ薦めるな、って話を書いた事があるが、OSを入れ替える(あるいはそれを代替する行為)、とか言うのはそれ以上にハードルが高い、ってのは別に説明せんでも分かるだろう。iMac購入とか、エロゲも出来ないようなPCを買う事を学生に強制するなんつー非人道的な事が出来るわきゃねーだろ、ってな話だ。
だからOCamlを薦められないわけだ。

まぁ、ハッキリ言っておくけど、本がいくら良くてもWindowsに存在しないソフトはこの世に存在せんのと全く同じだ。これが原則。
こう書くと「いや、UNIX系には優秀なソフトがあって・・・」とか言い出すヤツが必ず現れるが、現実を見よう。単にユーザーがいないようなプラットフォームで動くソフトウェアなんざ無いも同じなんだよ。「ユーザー数が少ない」と言うのはそれだけで致命的だ、っちゅー事だ。使用者が10%未満くらいしかいないようなプラットフォーム及びそこで走るソフトウェアをマジメに捉える、なんつーのは馬鹿げてる。そしてエロゲも出来ないようなプラットフォームに実用性があるわけがないだろ。
まぁ、Linuxユーザーがこんな事言うのは異端だと言う事は分かってるが、反面、LinuxはWindowsにもソフトをたくさん輸出してるし、辛うじてエロゲも出来るんで、幾分かマシなだけだ。
まぁいずれにせよ、「プログラミングをする為だけに」プログラミング初学者にクソメンド臭い敷居を突破させる、なんつーのは馬鹿げてる。そんなわけでOCamlは悔しいけど、プログラミング初心者には薦められないわけだな。

とは言っても、上述の本は本当に優れている。ある程度プログラミングに慣れてる人には是非とも一回は目を通して欲しい本ではあるし、以前書いたようにお茶ノ水女子大学の方ではこの本を使ったビデオ講座も公開されている
一度視聴してみればいい。以前、動画でプログラミング言語を学ぼう、なんつーのは馬鹿げてる、と書いたが、そんな僕でもこのビデオ講座は非常に良く出来てる、って思う。現役のお茶の水女子大学の先生がキチンとプログラミングの基礎を教えてくれてるんだ。分かる?そこらの誰だか知らん「プログラマ」が教える、と言う畑違いの動画ではない、「プログラミング教育のプロ」が作ってる動画なんだ。当然クオリティはダンチだと思う。
それに、女の園であるお茶の水女子大学の先生だぜ?観てるこっちも女子大生になる気分を味わえる、ってこった(笑)。キャッキャウフフ(謎
バーチャルで女子大生になれる機会はそうそうねぇだろ(笑)。なったからなんだ、ってワケでもねーが(笑)。

ところで、だ。動画を観てみれば分かるが、この本は一律、「デザインレシピ」と言うフォーマットをまずは使ってプログラムを組んでいこう、と言う本だ。
デザインレシピ、では、プログラム(関数)を書き始める前にまずはコメントとして関数の目的と関数名、そして引数の型と返り値の型を記述する。
そして関数を書く前に「関数のテストを最初に」書く。
それから初めて、関数を書き始める、って事だ。

(* OCaml での例 *)

(* 目的 : 所持金が与えられたとき 126 円の
チョコレートをいくつ買えるかを求める *)
(* chocolate : int -> int *)

(* テスト *)
let test1 = chocolate 100 = 0
let test2 = chocolate 252 = 2
let test3 = chocolate 500 = 3

;; Racket での例

;; 目的 : 所持金が与えられたとき 126 円の
;; チョコレートをいくつ買えるかを求める

;; chocolate : number -> number

;; テスト
(= (chocolate 100) 0)
(= (chocolate 252) 2)
(= (chocolate 500) 3)

プログラムを書き始める前にここまで記述しておいて、それからプログラムを書き始めるわけだ。

実はこのデザインレシピ、この本のオリジナルじゃない。ネタをバラすと、実はこの「プログラミングの基礎」と言う本。How to Design Programsと言う本で提案されたデザインレシピ、と言う方針を借りてきている。そしてこの本、How to Design Programsは、覗いてみれば分かるだろうが、実はRacketの開発者達が書いたRacketによるプログラミング入門書なんだ。
つまり、Racket用の本での「プログラミング初心者向けの開発手法のノウハウ」が巡り巡って日本にやってきて、OCamlを使ったプログラミング初心者向けの本に取り込まれたわけだな。
だったら、最初からRacketでやってくれよ、とちと言いたいトコだが(笑)、「プログラミングの基礎」の作者は動的型付け言語より性的静的型付け言語の方が好きなんだろう。また、流行りもあって、1990年代は動的型付け言語の方が人気があったが、2000年代に入ってからは静的型付け言語の人気が上がってきた事もある。他の言語に異動するにせよ、静的型付け言語の方を最初に学んだ方が現在だと有利だ、と言うような判断だったんだろう。

いずれにせよ、これらで語られるデザインレシピ、つまり「プログラムの開発手法」はテストファーストと言う事だ。プログラム(関数)を書き始める前にテストを書け、と。
そしてそういう手法自体は何もデザインレシピが最初にもたらしたものではない。テストファーストの開発手法テスト駆動開発(test-driven development, TDD)と呼ぶ。

この「テスト駆動開発」と言う手法はそこそこ使われてるみたいで(特にJavaでの開発?)、この「テストを簡単に行える」フレームワークをユニットテストと呼ぶ(※2)。
ユニットテストを用いた開発では、とにかく「テストを通る事」を第一目標にする。そこではまずは、「どんな汚いコードだろうと」テストを通す、と言う事だ。「綺麗なコードにする」と言うのは「テストを全て通った後」で良い。
言い換えると、手順的には

  1. まずは関数にどんな動作をして欲しいかテストを記述する。
  2. どんなに汚いコードでもいいのでテストを通す事をまずは最大目標とする。
  3. コードのリファクタリング(綺麗なコードに書き直す事)は全てのテストを通してから。
Racketにもユニットテストを行えるフレームワークが付属として付いていて、この「テスト駆動開発」が行えるようになっている。ちょっとそれを解説しよう。

その前に。
ANSI Common Lispには数字をローマ数字に直して出力できる、と言う無駄な素晴らしい機能がある。

CL-USER> (format t "~{~@r ~}~%" '(1 2 3 4 5 6 7 8 9 10 50 100 500 1000 31 148 294 312 421 528 621 782 870 941 1043 1110 1226 1301 1485 1509 1607 1754 1832 1993 2074 2152 2212 2343 2499 2574 2646 2723 2892 2975 3051 3185 3250 3313 3408 3501 3610 3743 3844 3888 3940 2999))
I II III IV V VI VII VIII IX X L C D M XXXI CXLVIII CCXCIV CCCXII CDXXI DXXVIII DCXXI DCCLXXXII DCCCLXX CMXLI MXLIII MCX MCCXXVI MCCCI MCDLXXXV MDIX MDCVII MDCCLIV MDCCCXXXII MCMXCIII MMLXXIV MMCLII MMCCXII MMCCCXLIII MMCDXCIX MMDLXXIV MMDCXLVI MMDCCXXIII MMDCCCXCII MMCMLXXV MMMLI MMMCLXXXV MMMCCL MMMCCCXIII MMMCDVIII MMMDI MMMDCX MMMDCCXLIII MMMDCCCXLIV MMMDCCCLXXXVIII MMMCMXL MMCMXCIX
NIL
CL-USER>

このように、Racketで、数値をローマ数字に変換する関数を書いてみよう(※3)。
どうやって書けばいいのかまるっきり見当が付かない、って場合がユニットテスト、及びそれを利用したデザインレシピの出番だ。
まずはファイルの先頭にRacketのユニットテスト用フレームワークrequireしよう。

(require rackunit)

次に、デザインレシピに従って、関数の目的を記述する。

(require rackunit)

;; 目的 : 整数をローマ数字に変換する。

;; integer->roman : integer -> string

次に、テストを書くが、ユニットテストのチェック用関数、check-equal?を使ってみよう(※4)。

(require rackunit)

;; 目的 : 整数をローマ数字に変換する。

;; integer->roman : integer -> string

;; テスト
(check-equal? (integer->roman 1) "I")
(check-equal? (integer->roman 2) "II")
(check-equal? (integer->roman 3) "III")
(check-equal? (integer->roman 4) "IV")
(check-equal? (integer->roman 5) "V")
(check-equal? (integer->roman 6) "VI")
(check-equal? (integer->roman 7) "VII")
(check-equal? (integer->roman 8) "VIII")
(check-equal? (integer->roman 9) "IX")
(check-equal? (integer->roman 10) "X")
(check-equal? (integer->roman 50) "L")
(check-equal? (integer->roman 100) "C")
(check-equal? (integer->roman 500) "D")
(check-equal? (integer->roman 1000) "M")
(check-equal? (integer->roman 31) "XXXI")
(check-equal? (integer->roman 148) "CXLVIII")
(check-equal? (integer->roman 294) "CCXCIV")
(check-equal? (integer->roman 312) "CCCXII")
(check-equal? (integer->roman 421) "CDXXI")
(check-equal? (integer->roman 528) "DXXVIII")
(check-equal? (integer->roman 621) "DCXXI")
(check-equal? (integer->roman 782) "DCCLXXXII")
(check-equal? (integer->roman 870) "DCCCLXX")
(check-equal? (integer->roman 941) "CMXLI")
(check-equal? (integer->roman 1043) "MXLIII")
(check-equal? (integer->roman 1110) "MCX")
(check-equal? (integer->roman 1226) "MCCXXVI")
(check-equal? (integer->roman 1301) "MCCCI")
(check-equal? (integer->roman 1485) "MCDLXXXV")
(check-equal? (integer->roman 1509) "MDIX")
(check-equal? (integer->roman 1607) "MDCVII")
(check-equal? (integer->roman 1754) "MDCCLIV")
(check-equal? (integer->roman 1832) "MDCCCXXXII")
(check-equal? (integer->roman 1993) "MCMXCIII")
(check-equal? (integer->roman 2074) "MMLXXIV")
(check-equal? (integer->roman 2152) "MMCLII")
(check-equal? (integer->roman 2212) "MMCCXII")
(check-equal? (integer->roman 2343) "MMCCCXLIII")
(check-equal? (integer->roman 2499) "MMCDXCIX")
(check-equal? (integer->roman 2574) "MMDLXXIV")
(check-equal? (integer->roman 2646) "MMDCXLVI")
(check-equal? (integer->roman 2723) "MMDCCXXIII")
(check-equal? (integer->roman 2892) "MMDCCCXCII")
(check-equal? (integer->roman 2975) "MMCMLXXV")
(check-equal? (integer->roman 3051) "MMMLI")
(check-equal? (integer->roman 3185) "MMMCLXXXV")
(check-equal? (integer->roman 3250) "MMMCCL")
(check-equal? (integer->roman 3313) "MMMCCCXIII")
(check-equal? (integer->roman 3408) "MMMCDVIII")
(check-equal? (integer->roman 3501) "MMMDI")
(check-equal? (integer->roman 3610) "MMMDCX")
(check-equal? (integer->roman 3743) "MMMDCCXLIII")
(check-equal? (integer->roman 3844) "MMMDCCCXLIV")
(check-equal? (integer->roman 3888) "MMMDCCCLXXXVIII")
(check-equal? (integer->roman 3940) "MMMCMXL")
(check-equal? (integer->roman 3999) "MMMCMXCIX")

ここまで書いて、初めてプログラムを書き始める。





ユニットテストにより何度もエラーを喰らうだろうが、完全にテストを通過すると実行しても、Racketインタプリタには何も表示されなくなる。



これでプログラムが完成した、と言う事になる。
如何だろうか?「☓☓するプログラムを書いてみたい」と言っても、具体的に何から書きはじめなければならないか、イマイチピンと来ない場合、デザインレシピに従って最初にテストを記述する「テストファースト」を徹底する方が取っ掛かりとしては上手く行く可能性が上がる、とは言えると思う。
もう1つのポイントは、ユニットテスト、と言うだけあって、小さい部品としての関数をテストするのが目的になる。従って、「なるたけ小さい関数を書く」と言うクセを付ける事が出来るだろう。小さい部品を作ってそれらを徐々に組み合わせて行く、と言うのは良い習慣になると思う。
また、デザインレシピは「関数の説明」を強制的に書かせる為、「コメントを記述する」と言うような習慣を付ける訓練にもなるはずだ。

ただし、個人的にはテスト駆動開発にも欠点があると思う。主だったトコは以下のような事だ。

1. テストを書くのがメンド臭い(笑)。

これはまんまそのまんまだ(笑)。業務ならいざ知らず、個人でプログラミングするには大げさだろ、と言うのがある。
また、「適切なテストを考える」と言うのも難しいし、そもそも「間違ったプログラミングをする」人間が「正しいテストを書けるのか」と言う問題がある(笑)。書いたテストが間違っていれば当然「バグがないプログラムである」と言う保証にならない。
それに「ある計算がしたい」としてテストを書くとして、その計算がややこしかった場合、その計算は何に行わせるんだろう?
例えば、数値微分の解なんかはどうだろうか。その「解」が何桁で、どういう数値が出るのか分かってるとしたら、それは殆どそのプログラムを「書き終えてる」のと同義だと思う。

2. 参照透過性があるプログラムにしか使えないのではないか。

ある関数を書いた時、ある値を与えれば決まった解が返る、と言うのがザックリとした参照透過性の説明だ。これは大方の関数に対しては正しいだろう。
ただし、場合によってはそうじゃない関数もある。代表的なトコでは乱数を使ったプログラムがあるだろう。これは決まった実引数を与えて毎回同じ結果ならむしろ困ったプログラムとなる。こういう場合はどうすんだろ?
もちろん、シードを固定して書く、ってのはあり得るやり方なんだけど、じゃあ完成したら、そのシードを排除する、ってぇんでいいんだろうか。実用的にはいいだろう、とは思うんだけど、かなり気持ち悪い。
また、手続きの場合はどうするんだ、と言う事もある。返り値がないブツを書かなければならない場合、ユニットテストは崩壊するんじゃなかろうか。

3. 本当に穴がないテストが書けるのか。

例えば上のプログラムは、1から3,999までの数は間違いなくローマ数字に変換してくれる。
ところが4,000を超えると変換が破綻するのだ。
言い換えると、このテストは4,000より小さい数値しか想定してなかった、と言う事になる。
これはテストファースト、と言う手法でも、穴があるテストだった場合、テストをパスしてもプログラムがいつでも正しく動いてくれる、と言う保証はない、と言う事になる。
本当は、完璧なテストを書く、と言う方がプログラムを書くより難しいんじゃないか、と言う不安がある。
もちろん、新たにテストを追加して「より完璧なプログラムを書こう」と言う事は出来なくはない。ただしその場合、単純に、テストファーストと言う方法論の提言自体が破綻する、と言う意味に他ならない。

大まかにはこの三点が怪しい、とは思ってるんだけど、取り敢えずプログラミング初心者で「プログラムをどう書くか取っ掛かりが掴めない」と言う人たちには、指針を得る為には有用な方法論、だとは思っている(※5)。


※1: これが理由で、プログラミング初学者にはSchemeも薦められない。Schemeを使った良いプログラミング入門書籍が皆無だから、だ。
市販されてる本を考慮すると、SchemeをやるならANSI Common Lispを最初に学んだ方が格段に敷居が下がるとは思う。ただし、難点は、LispはEmacsと結びつき過ぎてて、ここでもIDEの問題が出てくるのがイマイチなんだ。

※2: もちろん、ユニットテストとは単体の関数をテストする事を指すが、現在ではユニットテスト用のフレームワーク、と言う「ソフトウェアの種類」をユニットテストと呼称する事も多いようだ。

※3: 元ネタは Dive into Python3 に拠る。

※4: チェック用関数はたくさんある。詳しくはRackUnit APIを参照の事。

  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事