« [コラム] F# で NUnit するには | トップページ | [ブログ紹介] やる夫で学ぶ TDD »

2011年1月17日 (月)

[コラム] F# で TDD (@TDD道場#06)

わんくま名古屋勉強会#16 (2011/1/15) の TDD 道場では、 F# で FizzBuzz をやりました。
登壇していただいた 4名の方、 ありがとうございました。 会場には F# を見るのも初めてという方が多かったかと思いますが、 楽しんでいただけましたでしょうか。

テストコードの全体と、 製品コードの途中までは、 「[コラム] F# で NUnit するには」 に掲載してあります。 また、 当日のスライドとソースコードは、 「勉強会などで使った資料」 からダウンロードできます。 ここでは、 コードが成長していく様子と、 最後のリファクタリングについて書いてみます。

テストファーストでは、 失敗する (NUnit ランナーがレッドを表示する) テストケースをひとつ書いて、 それを通すように製品コードを追加・修正してそのテストケースを成功させます (グリーン)。 そのステップを示すと、 FizzBuzz では次のようになります。 (太字にして色を付けた部分が、 そのステップで変更・追加したところ。)

テストケース】
  [<TestCase(1, "1")>]
  member x.SayTest (n:int) (expected:string) :unit =
    Assert.AreEqual(expected, FizzBuzz.Say(n))

【製品コード】
let Say n =
  "1"

【テストケース】
  [<TestCase(1, "1")>]
  [<TestCase(2, "2")>]
  member x.SayTest (n:int) (expected:string) :unit =
    Assert.AreEqual(expected, FizzBuzz.Say(n))

【製品コード】
let Say n =
  n.ToString()

【テストケース】
  [<TestCase(1, "1")>]
  [<TestCase(2, "2")>]
  [<TestCase(3, "Fizz")>]
  member x.SayTest (n:int) (expected:string) :unit =
    Assert.AreEqual(expected, FizzBuzz.Say(n))

【製品コード】
let Say n =
  if n % 3 = 0 then "Fizz"
  else n.ToString()

【テストケース】
  [<TestCase(1, "1")>]
  [<TestCase(2, "2")>]
  [<TestCase(3, "Fizz")>]
  [<TestCase(5, "Buzz")>]
  member x.SayTest (n:int) (expected:string) :unit =
    Assert.AreEqual(expected, FizzBuzz.Say(n))

【製品コード】
let Say n =
  if n % 3 = 0 then "Fizz"
  elif n % 5 = 0 then "Buzz"
  else n.ToString()

【テストケース】
  [<TestCase(1, "1")>]
  [<TestCase(2, "2")>]
  [<TestCase(3, "Fizz")>]
  [<TestCase(5, "Buzz")>]
  [<TestCase(15, "Fizz Buzz")>]
  member x.SayTest (n:int) (expected:string) :unit =
    Assert.AreEqual(expected, FizzBuzz.Say(n))

【製品コード】
let Say n =
  if n % 15 = 0 then "Fizz Buzz"
  elif n % 3 = 0 then "Fizz"
  elif n % 5 = 0 then "Buzz"
  else n.ToString()

完成! テストファースト終了。


製品コードが望みの応答を返してくれるようになったので、 一応は完成です。
テストケースをひとつづつこなしていくと、往々にしてこのように if 文の連続になってしまうことがありますが、 これはあまり美しくありませんね。 リファクタリングしたくなります。

最初の if 文の条件は、 n % 15 = 0 となっていますが、 これは n % 3 = 0 && n % 5 = 0 であるとも言えます (問題の意味的には、 後者が本来の姿)。 二つ目は n % 3 = 0 ですが、 これは n % 3 = 0 && n % 5 <> 0 と書いても構わないはずです (n % 5 <> 0 は常に True と評価されますが)。 同様にして、 製品コードを次のように書き直しても構わないでしょう。

let Say n =
  if n % 3 = 0 && n % 5 = 0 then "Fizz Buzz"
  elif n % 3 = 0 && n % 5 <> 0 then "Fizz"
  elif n % 3 <> 0 && n % 5 = 0 then "Buzz"
  elif n % 3 <> 0 && n % 5 <> 0 then n.ToString()


上のコードを実際に書くことはないでしょうが、 このような形に書けると分かれば、 パターンマッチ に置き換えることが出来ると気づきます。 すなわち、 次のようにリファクタリングできます。
※ あえてワイルドカードは使っていません。

let Say n =
  match (n % 3 = 0, n % 5 = 0) with
  | (true,  true)  -> "Fizz Buzz"
  | (true,  false) -> "Fizz"
  | (false, true)  -> "Buzz"
  | (false, false) -> n.ToString()


このように F# では、 if 文の連鎖を見たらパターンマッチを考えてみるのがひとつのパターンになっています。
※ そのため、 F# では FizzBuzz に文字列連結や "If (output.Length = 0) Then" ( → リファクタリングはどっちへ進むべきだろう? ) などというコードが登場する余地はありません。


ところで上のパターンマッチによるコード、 何かに似ていませんか?
古株にはピンと来る人も多いでしょう。 そう、 デシジョンテーブルそのものです。
※ このデシジョンテーブルは自己流です。 JIS X 0125 の書き方が正式なんでしょうが、 冗長なので好きじゃありません

FizzBuzz のデシジョンテーブル
3の倍数5の倍数出力
Yes Yes "Fizz Buzz"
Yes No "Fizz"
No Yes "Buzz"
No No その数字

F# = 関数型言語 → 数学向き → 業務アプリケーションには不向き
…という連想をされる人もいるようですが、 このようにデシジョンテーブルをそのままコードとして表現できるなど F# は論理を表現するのに優れた言語でもあって、 業務アプリケーションのロジック部分にはむしろ向いているのではないかと思います。

|

« [コラム] F# で NUnit するには | トップページ | [ブログ紹介] やる夫で学ぶ TDD »

*コラム」カテゴリの記事

<NUnit>」カテゴリの記事

コメント

この記事へのコメントは終了しました。

トラックバック


この記事へのトラックバック一覧です: [コラム] F# で TDD (@TDD道場#06):

« [コラム] F# で NUnit するには | トップページ | [ブログ紹介] やる夫で学ぶ TDD »