TDD Boot Camp 東京 1.6 に行ってきた
行ってきました。
当日は .NET の TA をやるはずでしたが、希望者がいませんでした。残念。
暇になったので、C++ と Java の混合グループに入れてもらって、一人で F# やってました*1。
ソースは github に上げてあります。
bleis-tift/TDDBCTokyo1.6 · GitHub
timeup というタグまでが、会場でやったところです。
それ以降は、ホテルに戻ってからの分で、一応全部のお題をやってあります*2。
開発は、
- Visual Studio 2010
- NaturalSpec / NUnit
- Git / git-now / Git-Hooks
- メモに TODO リスト
な感じでやりました。
NaturalSpec は使い込み度合いがまだまだなので、色々試しながら進めました。
順を追って見てみる
最初の方は割と細かく進めたので、順を追って見てみます。長いです。あと割と誰得感が強いです。
基本的にテストではなく実装の移り変わりを追っていきます。
テストの方は手元で Git を使って随時確認していってください。
お題は
- put で key と value を追加し、dump で一覧表示、get で key に対応する value を取得
- delete で指定の key-value を削除
- put の引数に既に存在する key が指定された場合、value のみを更新する
- key と value のセットを一度に複数追加できる
の 4 つが最初与えられました。
最初の判断は、データの保持にどういう型を使うかです。
機能的には Map を使えば一瞬で終わりそうです。
しかし、dump を考えると list の方が後々楽かなー、と思って、とりあえず list を使ってやってみることにしました。
ざっと全部確認しつつ、上から順番にやっていくことにしました。
まず、TODO リストにやることを書きだします*3。
最初のお題は割と大きいので、「KVSの生成と追加」、「一覧表示」、「取得」と区切ることにして、まずは KVS の生成と追加から実装することにしました。
- 空の KVS の生成
- 空の KVS への追加
- 重複しないペア*4の追加
- 重複するペアの追加
この中で、最後の項目は 3 つ目のお題でやることになります。
空の KVS の生成
Git の SHA-1 ハッシュでいうと、b818 のコミットです。
これは API を決めるだけの単純なものなので、コミット一回で終了です。
今回作る KVS は、list を操作する関数群として実装することに決めました。
そのため、empty の実装は空リストそのものとなります。
module KeyValueStore let empty = []
TODO リストの先頭を消します。
空の KVS への追加
6ba1 から 4ffc までの、5 回 git-now しています。
let put k v kvs = [k, v]
キーと値と KVS を受け取って、キーと値のペアを一つ含む list を返すだけの実装です。
2 つの要素を含む list を作っているように見えるかもしれませんが、F# では list の要素の区切りはセミコロンです。
まだ完全ではありませんが、空の KVS への追加という仕様は満たしているので、TODO リストの 2 番目の項目を消します。
empty と put を作ったので、この KVS への登録方法を示すことができるようになりました。
let kvs = KeyValueStore.empty |> put 10 "hoge" |> put 2 "piyo"
これは、空の KVS に (10, "hoge") と (2, "piyo") を登録することを表しています。
(|>) という演算子によって、f x と書く代わりに x |> f と書けるようになります。
put は 3 つの引数が必要ですが、put 10 "hoge" とまず 2 つ渡しておき*5、第三引数としては KeyValueStore.empty を渡していることになります。
さらに、put 2 "piyo" と 2 つ渡しておいたものに、さっき返ってきた KVS を第三引数として渡します。
以上のことから、この KVS は状態を保持しないことがわかります。
put は引数で指定されたペアを登録した新しい KVS を返します。
また、状態を持たずに put で新しい KVS を返すため、put のテストのために get を実装したり、内部状態を外部に晒す必要がありません。
重複しないペアの追加
5230 から 0895 までの、3 回 git-now しています。
let put k v kvs = (k, v) :: kvs
(::) 演算子は、右項の list の先頭に左項の要素を追加した、新しい list を返す演算子です。
非常に単純ですね。
キーが重複しない範囲では動くようになったので、TODO リストの 3 つ目の項目を削除します。
git now --rebase
ここまでで、最初のお題の put の部分は完成ですので、このタイミングで git now --rebase しておきました。
git now --rebase する前のコミットには、put タグを打ってあります*6。
git-now について触れていませんでしたが、これは git の非標準コマンドです。
詳しくは tmp コミットのための独自サブコマンド git-now - アジャイルSEを目指すブログ をどうぞ。
TODO リストの更新
TODO リストの今の状態は、
空の KVS の生成空の KVS への追加重複しないペアの追加- 重複するペアの追加
となっています。
ここで、一覧表示用の TODO を追加しておきます。
- dump で一覧表示
- toStr で文字列化
dump は「表示」というテストしにくい厄介なものを扱います。
ここで、いくつかの選択肢があるわけですが、今回は「toStr で文字列化して、dump は toStr を呼び出すだけの単純な実装にする」という方法を採用しました。
実は TODO リストには、
- list で文字列化
- rawData()
などと言った項目も書いてあるのですが、バツを付けてあります。
TDD ではテストによって設計判断を下すことも多いのですが、TODO リストもその判断の材料になります*7。
まずは TODO リストに書き出してみて、即捨てる、といことをよくやります。
ちょっとでも迷ったら、実際にテストを書いてみます。
toStr で文字列化
6290 から ed28 の 3 回 git-now しました。
let toStr kvs = sprintf "%A" kvs
%A は、タプルやリストなどと言った型の値をいい感じに文字列化してくれます。
楽ちんですね。TODO リストから、「toStr で文字列化」を消します。
dump で一覧表示
a390 の 1 回だけ git-now しました。
let dump kvs = do printf "%s" (kvs |> toStr)
受け取った KVS を toStr で文字列化し、printf で出力しているだけです。
do というのは、副作用のある部分に対する目印のようなもので、別になくても構いません。
なんだかわざわざ toStr 作る必要なかったような感じですね。
ただ、これで toStr として文字列を組み立てる部分のテストが簡単に書けました。
なんか後で形式の変更だとか、順番の指定だとかが来たら怖いな、という不安はあったので、それを取り除くためにこうなった、という感じです。
TODO リストから「dump で一覧表示」を消します。
ここまでを git now --rebase して、次へ進みます。
これまでの git-now のコミットには、dump タグを打ってあります。
TODO リストの更新
TODO リストの今の状態は、
空の KVS の生成空の KVS への追加重複しないペアの追加- 重複するペアの追加
dump で一覧表示toStr で文字列化
となっています。
ここで、取得の TODO を追加しておきます。
- 空の KVS から取得すると None
- 一致するキーがある KVS から取得すると Some に包まれた値
- 一致するキーが無い KVS から取得すると None
F# では、値の有る無しを option という型で表します。
ここでは、その option を戻り値の型にすることにしました。
None は値が無いことを、Some x は x という値があることを表します。
option を使うことで、null よりもより安全に「値が無いかもしれない」ということを表せるわけですが、そのあたりは
とかをどうぞ。
空の KVS から取得すると None
457a の一回 git-now しました。
let get k kvs = None
仮実装ですね。
TODO リストから項目を削除します。
一致するキーがある KVS から取得すると Some に包まれた値
f286 から ccaf の 3 回 git-now しています。
まずは、仮実装を Red にするテストを追加し (f286)、そのテストを Green にする実装に変更しています (2416)。
let get key kvs = kvs |> List.tryFind (fun (k, _) -> k = key) |> Option.map snd
これで Green を確認し、TODO リストから削除します。
よく考えたら tryFind の引数も fst で書けると思い、リファクタリングしました (ccaf)。
let get key kvs = kvs |> List.tryFind (fst >> ((=)key)) |> Option.map snd
List.tryFind は、第一引数を満たす要素が存在する場合は Some に包んで返し、存在しない場合は None を返します。
ここでは引数で渡されたキーと同じものがあるかどうかなので、最初は
fun (k, _) -> k = key
と、キーだけ取り出して引数と比較する関数を渡しています。
これを fst という、ペアの先頭要素を取り出す関数と、引数との比較関数の関数合成の形に変えています。
(>>) は、右項の関数の結果を左項の関数の引数として渡す関数を作り出します。
f(g x) を、(g >> f) x と書けるわけです。引数である x を一番外側に追い出せたので、(g >> f) だけでも値として完結しています。
値なので関数に渡すことができます (高階関数)。
List.tryFind だけではペア (の option) が返ってきますが、欲しいのは値だけです。
なので、Option.map を使って値に変換しています。
Option.map は、None に対しては常に None を返すので、Option.map に渡す関数は Some の場合だけ考慮すればいいことになります。
さらに、Some の場合は Some をはぎ取って渡してくれるので、単純にペアがわたってくるものとして書けます。
こちらは最初から snd を使っていますが、ラムダ式を使って書くと、
fun (_, v) -> v
となります。
一致するキーが無い KVS から取得すると None
9c22 の一回だけ git-now しました。
List.tryFind は、空の list に対しては常に None を返すので、実装は何も弄る必要はありません。
TODO リストから項目を削除します。
リファクタリング
ここら辺で、テスト側でよく出てくる
KeyValueStore.empty |> put k v
という記述が気になり始めました。
いちいち空の KVS に put していくよりも、初期状態を簡単に構築できた方が便利だと思い、init 関数を用意することにしました。
f636 でテストと実装を追加して、
let init kvs = kvs
9e57 でテストを init を使うように書き直しました。
KVS 自体が単なる list なので、実装はとても簡単ですね。
これだけのことで、
let kvs = KeyValueStore.empty |> put 1 "hoge" |> put 2 "piyo"
を
let kvs = KeyValueStore.init [1, "hoge"; 2, "piyo"]
のように書けるようになりました。
ここまでで、git now --rebase しておきます (get と init でコミットを分けました)。
git-nowのタグは get です。
これで、一つ目のお題が完成です。
TODO リストは、
空の KVS の生成空の KVS への追加重複しないペアの追加- 重複するペアの追加
dump で一覧表示toStr で文字列化空の KVS から取得すると None一致するキーがある KVS から取得すると Some に包まれた値一致するキーが無い KVS から取得すると None
のようになりました。
TODO リストの更新
次のお題は、「delete で指定の key-value を削除」です。
TODO リストに
- 空の KVS から削除しても空
- 一致→取り除いた KVS
- 一致なし→そのまま
を追加しました。メモなので、自分が分かればいいのでてきとーな感じです。
このとき、もうすぐ前半戦終了だったので急いでいたのでしょう。
空の KVS から削除しても空
d7c0 の一回だけ git-now しています。
let delete key kvs = []
仮実装です。TODO リストから項目を削除します。
一致→取り除いた KVS
a74a の一回だけ git-now しています。
let delete key kvs = kvs |> List.filter (fst >> ((<>)key))
単純ですね。キーが引数と異なるものだけを残しています。つまり、キーが同じものを取り除いた KVS を生成して返しています。
TODO リストから項目を削除します。
リファクタリング
ここで、get と delete の重複が気になりました。
List.tryFind (fst >> ((=)key)) // 略 List.filter (fst >> ((<>)key))
この部分です。
2146 では、これをまとめるリファクタリングを行っています。
let private eq key (k, _) = k = key // 略 List.tryFind (eq key) // 略 List.tryFind (eq key >> not)
一致なし→そのまま
TODO リストには「一致なし→そのまま」という項目が残っています。
が、実装を見てみると
let delete key kvs = kvs |> List.filter (eq key >> not)
と、一致するキーが無ければ何も起こらないことは明白です。
この部分は不安にならなかったため、何もせずに TODO リストから削除しました。
git now --rebase して、2 つ目のお題完成です。
git-now のコミットのタグは、delete です。
重複するキーの追加
TODO リストは、
空の KVS の生成空の KVS への追加重複しないペアの追加- 重複するペアの追加
dump で一覧表示toStr で文字列化空の KVS から取得すると None一致するキーがある KVS から取得すると Some に包まれた値一致するキーが無い KVS から取得すると None
こうなっています。
3 つ目のお題は「put の引数に既に存在する key が指定された場合、value のみを更新する」ですので、ここで残っている項目がやっと消化できます。
重複するペアの場合は、value のみを更新するようです。
2e72 の一回の git-now で実装しました。
(* 以前の実装 let put k v kvs = (k, v) :: kvs *) let put k v kvs = if kvs |> List.exists (eq k) then (k, v) :: (kvs |> List.filter (eq k >> not)) else (k, v) :: kvs
これはひどい・・・
37cc で即リファクタリングです。
let put k v kvs = (k, v) :: (kvs |> List.filter (eq k >> not))
この間 30 秒・・・
これで git now --rebase して、お題 3 終了です。
git-now のコミットのタグは、put2 です。
TODO リストの更新
TODO リストに項目は残っていないので、新しいものだけを示します。
- putAll で複数登録できる
- 既に存在するキーは値が上書き
- 指定した引数内にキーの重複があったら後勝ち
putAll で複数登録できる
6d37 一回で完了です。
let putAll xs kvs = xs |> List.fold (fun st (k, v) -> st |> put k v) kvs
List.fold は、第二引数 (kvs) を初期値にして第三引数 (xs) の各要素 (k, v) に第一引数で指定した関数を適用していきます。
第一引数の関数の引数 st には、一番最初は初期値 (つまり kvs) が、次からは前回の結果 (つまり st |> put k v の結果) が入ってきます。
これで、kvs に対して xs のすべての要素を put した KVS が生成されます。
TODO リストから削除します。
今思うと、
let putAll xs kvs = (kvs, xs) ||> List.fold (fun st (k, v) -> st |> put k v)
の方が分かりやすかったかもしれません。
既に存在するキーは値が上書き、指定した引数内にキーの重複があったら後勝ち
これらは一応テストを追加しましたが、putAll の実装は既にこれらの仕様を満たしています。
用意された list 用の高階関数を使うと、手続的な書き方をしていては考えなければならないことを何も考えずに満たせるということがよくあります。
なら例外には弱いんじゃないの?と思われるかもしれません。
例えば、「putAll は先勝ち」となっていた場合、xs を反転 (List.rev) するだけで満たせます。
TODO リストをすべて終えました。
ここまでで、プロダクトコードは 27 行、テストコードは 110 行です。短いですね。
仕様の追加
ここからは仕様の追加への対応です。
put で key と value を追加し、dump で一覧表示、get で key に対応する value を取得delete で指定の key-value を削除put の引数に既に存在する key が指定された場合、value のみを更新するkey と value のセットを一度に複数追加できる- put の引数で key,value,date を渡し、dump を時間順に出力するように仕様変更
- dump の引数に時刻を指定できるようにする。dump 関数は時刻が指定された場合、指定時刻以降のデータのみを表示する
- delete の引数に分・秒を指定できる。delete は分を指定された場合、「現在時刻 - 指定の分・秒」よりも古いデータをすべて削除する*8
そして、5 のバグとして
- put の引数から時刻を取り除き、現在時刻を使用するようにする
という仕様変更が入りました。
つまり、5 は date を指定するのではなく、今までの put のまま、内部で現在時刻を入れ込む形です。
これらの追加された仕様は面倒です。
今回は内部状態を持たず、list をさらけ出した形で実装しています。
なので、まずはこの仕様を満たしやすい形に構築しなおす必要があります。
TODO リストを新しくして、次の項目を追加しました。
- テスト用にダミーの dt を追加
- 返すのは (key, value, dt) の list
- テストでは dt に適当なデータを突っ込んでテスト
- 時刻を指定する put を追加
- dump が新→旧の順番で出力するようにする
テスト用にダミーの dt を追加
29f2 のコミットです。
let mutable dt: DateTime option = None let now () = match dt with Some dt -> dt | None -> DateTime.Now
dt はデフォルトで None になっており、その状態で now 関数を呼び出すと現在時刻が返されます。
テストでは dt に適当な時刻を設定し、その状態で now 関数を呼び出すと設定した時刻が返されます。
TODO リストの項目を削除します。
返すのは (key, value, dt) の list
eb29 のコミットです。
今までは ('a * 'b) list だったところを ('a * 'b * DateTime) list に変更しています。
(* let init kvs = kvs *) let init kvs = kvs |> List.map (fun (k, v) -> k, v, now()) (* let private eq key (k, _) = k = key *) let private eq key (k, _, _) = k = key let put k v kvs = (* (k, v) :: (kvs |> List.filter (eq >> not)) *) (k, v, now) :: (kvs |> List.filter (eq >> not)) let get key kvs = kvs |> List.tryFind (eq key) (* |> Option.map snd *) |> Option.map (fun (_, v, _) -> v)
コンパイルは通りませんが、TODO リストの項目は満たしているので、TODO リストから削除します。
テストでは dt に適当なデータを突っ込んでテスト
3a2a のコミットです。
KeyValueStore.dt に固定値 (defaultDt) を設定し、テストの期待値にも defaultDt を各ペアの末尾に入れました。
これで、コンパイルも通り、テストもオールグリーンに戻りました。
TODO リストから削除します。
時刻を指定する put を追加
9ce9 のコミットです。
仕様変更によって時刻を指定する put は本来不要なのですが、これがあるのとないのとではテストのしやすさがまったく違います。
なので実装してしまいます。
let putWithDt k v dt kvs = (k, v, dt) :: (kvs |> List.filter (eq k >> not))
put と中身が似ているので、put の実装を putWithDt を使って書き直します(0e01)。
let put k v kvs = (* (k, v, now()) :: (kvs |> List.filter (eq k >> not)) *) kvs |> putWithDt k v (now())
TODO リストから削除します。
ここで、タイムアップでした。
コードはこんな感じです。
TDDBCTokyo1.6/solution/NagoyaFSharp/KeyValueStore.fs at timeup · bleis-tift/TDDBCTokyo1.6 · GitHub
型を書いてあるのが、5 行目の DateTime option だけとなっています。
とても静的型付けの言語には見えないかもしれませんが、型推論によって全て型が付いています。
また、一つ一つの関数の長さも、一つを除いてすべて本体が 1 行、後の一つさえ本体が 3 行、全体として空行を入れても 36 行と、とてもコンパクトに実装されています。
これは、高階関数と関数合成、null を考慮しなくてもいい*9というのが大きく効いているのだと思います。
この状態で模範解答として皆さんの前で発表したわけですが、「プログラムに見えなかった」という声もありました。
手続型のプログラムとは考え方が全く違うから、ということもできますし、より宣言的であるから、とも言えるでしょう。
ここからはホテルに帰って、(Twitter をしながら) 一応最後まで実装した部分になります。
だめだ、コマンドさえまともに組み立てられない。git rebase --amendとやりそうになって、git commit --continueとやりそうになって、ようやくgit commit --amendにたどり着いた
こんな状態で実装してます。
dump が新→旧の順番で出力するようにする
今の実装では、KVS はすでに時間順に並んでいることになります。
なので、そのままでも動くと思っていたんですが、テストを追加 (67c4) すると Red でした。
init の実装が引数の list をそのままの順番で登録しているのがまずかったので、これを修正 (215f) しました。
let init kvs = kvs |> List.map (fun (k, v) -> k, v, now()) |> List.rev
list を逆順にしただけです。
init の返す順番に依存していたテストが Red になったので、これらを修正 (8f03 から ac4e の 5 コミット) しました。
修正中に「empty に putAll でいいじゃん」ということに気付いて、TODO リストに「putAll で書き直し」とし、修正が終わってから書き直しました (55d8)。
let init kvs = empty |> putAll kvs
git now --rebase して、TODO リストから「putAll で書き直し」と「dump が新→旧の順番で出力するようにする」を削除します。
git-now のコミットのタグは dump2 です。
時刻指定の dump
これは 時刻指定の toStr を作れば簡単です。
9113 で toStrFrom を追加し、978d でそれを使った dumpFrom を追加しました。
let toStrFrom t kvs = kvs |> Seq.takeWhile (fun (_, _, dt) -> t <= dt) |> Seq.toList |> sprintf "%A" let dumpFrom t kvs = do printf "%s" (kvs |> toStrFrom t)
時間指定の delete
これ、このエントリ書いてて気づいたんですけど、全然違う実装してました。
本来なら、「分と秒を指定して、現在時刻から指定分、秒以前のデータを削除」なのですが、実装したのは「指定時刻以前のデータを削除」です。
まぁこいつを使えば本来必要だったものは簡単に実現できるので許して下しあ><
4f71 から a12e の 3 回 git-now しました。
最後の git-now では、toStrFrom の実装を deleteUntil と toStr を使うようにリファクタリングしています。
let deleteUntil t kvs = kvs |> Seq.takeWhile (fun (_, _, dt) -> t <= dt) |> Seq.toList let toStrFrom t kvs = kvs |> deleteUntil t |> toStr
これは、内部状態を持たないからこそできることですね。
git now --rebase して、お題 7 まで完了です。
git-now のコミットのタグは、deleteUntil です。
ここまでで、プロダクションコードは 45 行、テストコードは 162 行です。
相変わらず、get のみ本体 3 行、他は全て本体 1 行となっています。
fsi の追加
ここまでほとんど型を書かずに進めてきましたが、ドキュメントとしての型が欲しいところです。
また、外部からはどれが使えるのか、という情報も欲しいです。
そのために、F# ではシグネチャファイルと言うものが使えます。
9371 で自動生成したものをちょっとだけ弄ったやつを追加していますが、外部からどれが使えるかはわかりますけど、ドキュメントの役には立ちそうにありません。
これを、565e でもっと役に立ちそうな形に整えました。
module KeyValueStore open System type Pair<'a, 'b> = ('a * 'b) type KVS<'a, 'b> = ('a * 'b * DateTime) list // 公開API val empty: 'a list val init: Pair<'a, 'b> list -> KVS<'a, 'b> when 'a: equality val put: 'a -> 'b -> KVS<'a, 'b> -> KVS<'a, 'b> when 'a: equality val putAll: Pair<'a, 'b> list -> KVS<'a, 'b> -> KVS<'a, 'b> when 'a: equality val get: 'a -> KVS<'a, 'b> -> 'b option when 'a: equality val delete: 'a -> KVS<'a, 'b> -> KVS<'a, 'b> when 'a: equality val deleteUntil: DateTime -> KVS<'a, 'b> -> KVS<'a, 'b> val dump: KVS<_, _> -> unit val dumpFrom: DateTime -> KVS<_, _> -> unit // 以下テスト用 val mutable internal dt: DateTime option val internal putWithDt: 'a -> 'b -> DateTime -> KVS<'a, 'b> -> KVS<'a, 'b> when 'a: equality val internal toStr: KVS<_, _> -> string val internal toStrFrom: DateTime -> KVS<_, _> -> string
これによって実装が 3 行増えて 48 行になりました。
fsi ファイルはあくまでドキュメントで、消しても動きます。
また、fsi ファイルがある状態だと、fs ファイルとの間で型が正しいかどうかのチェックが行われ、型が合わない場合はコンパイルエラーになります。
そのため、fsi ファイルは「本当に自分の望む型になっているか」のチェックに使えます。
今回は型はほとんど意識せずに進めたので、自分の想定よりもより広い型を受け入れるような状態でしたので、実装側にもちょっとだけ型に関する情報を追加して、自分の思っていた型に推論されるように調整しました。
テストのグループ化
今までほとんどテストは見てきませんでしたが、フラットな感じでちょっと何がどこにあるのか分かり難いです。
NaturalSpec では (NUnit でも) ネストしたクラスもテスト対象になるので、グループごとにまとめました。
その際、すべてのサブモジュールで setup が実行されるように、NUnit の SetUpFixture 属性を使いました。
こんな感じになります。
module モジュール名 open NaturalSpec open NUnit.Framework [<SetUpFixture>] type SetUp() = [<SetUp>] member x.setup () = 全てのテストの前にやること module グループA = [<Scenario>] let テスト1 () = テストの中身 [<Scenario>] let テスト2 () = テストの中身 module グループB = ・・・
SetUpFixture 属性をサブモジュールに付けることができないのが残念な感じですが、割と便利な機能です。
まとめ
- 内部状態を持たないことで、put のテストが容易になった
- 内部状態を持たないことで、実装がコンパクトになった
- コードの重複はほとんどない
- データ構造をさらけ出したことで、保持する情報が増えたときにテストを Red にせずに対応することが困難
- 型をほとんど書いていないのに、推論によって型を付けてくれる
- C# などで組む時と比べ、感じる不安が少ない (そのため、テストの数もそれほど多くない)
- F# の素晴らしさを伝えることができてよかった
F#のすごさを見せつけられたので、ちょっと手をだしてみようかと思った。コードは引き継げなくなってしまうので、できないけど、テストコードなら気づかれずに実現できるかも。もっともF#の基本から始める必要があるんだけどね。#tddbc
*1:C++ と Java の TA 的なこともしてました。
*2:ひとつ勘違いがありますが・・・
*3:TODO リストは絶対ではなく、あくまでメモ代わりです
*4:キーと値、というと面倒なので、「ペア」という言葉を使うことにしました
*5:部分適用。カリー化じゃないよ。
*6:これは説明用で、いつもはタグは打っていません
*7:ので、TDD やる、やらないにかかわらずお勧めです
*8:エントリを書いていて気が付いたけど、このお題の意味を取り違えていた・・・まぁ眠かったから仕方がないということで
*9:.NET Framework のメソッドを使う場合などは null を考慮する必要がありますが、F# 内で閉じる今回のような場合、null の考慮は不要です。null が欲しい場面では常に option を使うのです。