« [コラム] リファクタリングとは、 最適化することじゃない。 理解しやすくすること。 | トップページ | [コラム] リファクタリングはどっちへ進むべきだろう? (続々) »

2010年10月21日 (木)

[コラム] リファクタリングはどっちへ進むべきだろう? (続)

元のエントリで、 TDD 道場でのリファクタリング結果 (の予想) をネタにしました。 じつはこのメソッドのカタチは、 午前中に行われたセッションの内容に影響を受けたとも思えます。 そこで登壇者のハンドルから Andochin バージョンと呼ぶことにします。 (オリジナルは C++ で書かれていました。 ⇒ FizzBuzzと貧乏性 )
また、 元記事のコメントには、 おがたん氏から違うアプローチのコードをいただきました。 こちらは Gwp バージョンと呼びましょう。

さて。 テストファーストやリファクタリングとは直接関係無い話をしてみます。 開発者としては、 最適化ということも面白いので。

元記事では、 「機械にやさしいより、 人にやさしい方がいいよね」 といったことを書きました。 それじゃネタにしたコードはほんとに機械に優しいのか、 つまり、 実行する処理が少なくなっているのか…?
それを見るために、 出来上がったアセンブリを ildasm.exe を使って逆アセンブルしてみましょう。 MSIL を読むことができなくても、 その量からある程度の判断はできるでしょう。

まずは、 Andochin バージョン。 最初に C# でのソースを示します。 (Gwp バージョンに合わせて、 全部 C# に直しました。

// Andochin バージョンの FizzBuzz.Say()
public static string AndochinSay(int n)
{
    if (n <= 0)
        throw new ArgumentOutOfRangeException();

    string s = string.Empty;
    if (n % 3 == 0)
        s += "Fizz";
    if (n % 5 == 0)
        s += "Buzz";
    if (s.Length == 0)
        s = n.ToString();

    return s;
}

これを C# 2010 で、 最適化を有効にしてビルドします。 出来上がった dll を ildasm.exe で逆アセンブルした MSIL コードは次のようになりました。 Code Size = 68 です。

.method public hidebysig static string
        AndochinSay(int32 n) cil managed
{
  // Code size       68 (0x44)
  .maxstack  2
  .locals init ([0] string s)
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.0
  IL_0002:  bgt.s      IL_000a

  IL_0004:  newobj     instance void [mscorlib]System.ArgumentOutOfRangeException::.ctor()
  IL_0009:  throw

  IL_000a:  ldsfld     string [mscorlib]System.String::Empty
  IL_000f:  stloc.0
  IL_0010:  ldarg.0
  IL_0011:  ldc.i4.3
  IL_0012:  rem
  IL_0013:  brtrue.s   IL_0021

  IL_0015:  ldloc.0
  IL_0016:  ldstr      "Fizz"
  IL_001b:  call       string [mscorlib]System.String::Concat(string, string)
  IL_0020:  stloc.0
  IL_0021:  ldarg.0
  IL_0022:  ldc.i4.5
  IL_0023:  rem
  IL_0024:  brtrue.s   IL_0032

  IL_0026:  ldloc.0
  IL_0027:  ldstr      "Buzz"
  IL_002c:  call       string [mscorlib]System.String::Concat(string, string)
  IL_0031:  stloc.0
  IL_0032:  ldloc.0
  IL_0033:  callvirt   instance int32 [mscorlib]System.String::get_Length()
  IL_0038:  brtrue.s   IL_0042

  IL_003a:  ldarga.s   n
  IL_003c:  call       instance string [mscorlib]System.Int32::ToString()
  IL_0041:  stloc.0
  IL_0042:  ldloc.0
  IL_0043:  ret
} // end of method FizzBuzzer::AndochinSay

 
次いで、 Gwp バージョンのソースコード。 ごらんの通り、 ラムダ式を上手く使って、 ごく短いソースになっています。

// Gwp バージョンの FizzBuzz.Say()
public static string GwpSay(int n)
{
    if (n <= 0)
        throw new ArgumentOutOfRangeException();

    Func<int, bool> Is倍数 = (x) => n % x == 0;
    return ((Is倍数(3)) ? "Fizz" : "") + ((Is倍数(5)) ? "Buzz" : "") + ((!Is倍数(3) && !Is倍数(5)) ? n.ToString() : "");
}

このほうが生成された MSIL も短くなるかと言えば、 そうでもないのです。 ラムダ式を使うとコードは簡潔になりますが、 裏方で苦労する機械には負荷を掛けることになります。 まず、 メソッド本体の MSIL を見てみます。 Code size = 125 です。

.method public hidebysig static string
        GwpSay(int32 n) cil managed
{
  // Code size       125 (0x7d)
  .maxstack  4
  .locals init ([0] class [mscorlib]System.Func`2<int32,bool> 'Is倍数',
           [1] class FizzBuzz.FizzBuzzer/'<>c__DisplayClass1' 'CS$<>8__locals2')
  IL_0000:  newobj     instance void FizzBuzz.FizzBuzzer/'<>c__DisplayClass1'::.ctor()
  IL_0005:  stloc.1
  IL_0006:  ldloc.1
  IL_0007:  ldarg.0
  IL_0008:  stfld      int32 FizzBuzz.FizzBuzzer/'<>c__DisplayClass1'::n
  IL_000d:  ldloc.1
  IL_000e:  ldfld      int32 FizzBuzz.FizzBuzzer/'<>c__DisplayClass1'::n
  IL_0013:  ldc.i4.0
  IL_0014:  bgt.s      IL_001c

  IL_0016:  newobj     instance void [mscorlib]System.ArgumentOutOfRangeException::.ctor()
  IL_001b:  throw

  IL_001c:  ldloc.1
  IL_001d:  ldftn      instance bool FizzBuzz.FizzBuzzer/'<>c__DisplayClass1'::'<GwpSay>b__0'(int32)
  IL_0023:  newobj     instance void class [mscorlib]System.Func`2<int32,bool>::.ctor(object, native int)
  IL_0028:  stloc.0
  IL_0029:  ldloc.0
  IL_002a:  ldc.i4.3
  IL_002b:  callvirt   instance !1 class [mscorlib]System.Func`2<int32,bool>::Invoke(!0)
  IL_0030:  brtrue.s   IL_0039

  IL_0032:  ldstr      ""
  IL_0037:  br.s       IL_003e

  IL_0039:  ldstr      "Fizz"
  IL_003e:  ldloc.0
  IL_003f:  ldc.i4.5
  IL_0040:  callvirt   instance !1 class [mscorlib]System.Func`2<int32,bool>::Invoke(!0)
  IL_0045:  brtrue.s   IL_004e

  IL_0047:  ldstr      ""
  IL_004c:  br.s       IL_0053

  IL_004e:  ldstr      "Buzz"
  IL_0053:  ldloc.0
  IL_0054:  ldc.i4.3
  IL_0055:  callvirt   instance !1 class [mscorlib]System.Func`2<int32,bool>::Invoke(!0)
  IL_005a:  brtrue.s   IL_0065

  IL_005c:  ldloc.0
  IL_005d:  ldc.i4.5
  IL_005e:  callvirt   instance !1 class [mscorlib]System.Func`2<int32,bool>::Invoke(!0)
  IL_0063:  brfalse.s  IL_006c

  IL_0065:  ldstr      ""
  IL_006a:  br.s       IL_0077

  IL_006c:  ldloc.1
  IL_006d:  ldflda     int32 FizzBuzz.FizzBuzzer/'<>c__DisplayClass1'::n
  IL_0072:  call       instance string [mscorlib]System.Int32::ToString()
  IL_0077:  call       string [mscorlib]System.String::Concat(string, string, string)
  IL_007c:  ret
} // end of method FizzBuzzer::GwpSay

ラムダ式を格納する Func<int, bool> Is倍数 というのはオブジェクトですから、 IL_0023: で new しています。 そして IL_002b: などで  System.Func`2<int32,bool>::Invoke(!0) として Is倍数() の呼び出しができているのです。 ちなみに、 Invoke(!0) を数えてもらうと分かりますが、 最適化を有効にしてコンパイルしたのに、 Is倍数() は全部で 4回 (ソースコードにあるとおりです) 呼び出されています。
さらに、 IL_0023: で Func<int, bool> Is倍数 を作るときに渡しているのは、 IL_0000: で new した '<>c__DisplayClass1' オブジェクトの持つメソッド '<GwpSay>b__0' です。 ( IL_001d: でロードしています。)
この '<>c__DisplayClass1' も、 Gwp バージョンの一部です。 次に掲載します。 int32 のフィールドをひとつ持ち、 メソッドのコードは合わせて Code size = 19 です。

.class auto ansi sealed nested private beforefieldinit '<>c__DisplayClass1'
       extends [mscorlib]System.Object
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
  .field public int32 n
  .method public hidebysig specialname rtspecialname
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method '<>c__DisplayClass1'::.ctor

  .method public hidebysig instance bool
          '<GwpSay>b__0'(int32 x) cil managed
  {
    // Code size       12 (0xc)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldfld      int32 FizzBuzz.FizzBuzzer/'<>c__DisplayClass1'::n
    IL_0006:  ldarg.1
    IL_0007:  rem
    IL_0008:  ldc.i4.0
    IL_0009:  ceq
    IL_000b:  ret
  } // end of method '<>c__DisplayClass1'::'<GwpSay>b__0'

} // end of class '<>c__DisplayClass1'

見当が付くと思いますが、 この '<GwpSay>b__0' がラムダ式の部分です。 ( IL_0001:  で n をロードし、 IL_0006: で引数 (x) をロードし、 IL_0007:  で剰余 (rem) を計算し、 IL_0008:, IL_0009: で 0 と比較しています。 )
ということで、 なんと Gwp バージョンの Code size は全部で 150 近くにもなってしまいました。

それでは、 最後に私のソース。

public static string Say(int n)
{
    if (n <= 0)
        throw new ArgumentOutOfRangeException();

    bool is3の倍数 = (n % 3 == 0);
    bool is5の倍数 = (n % 5 == 0);

    if (is3の倍数 && is5の倍数)
        return "FizzBuzz";
    if (is3の倍数)
        return "Fizz";
    if (is5の倍数)
        return "Buzz";

    return n.ToString();
}

そして、 MSIL。 Code size = 62 です。

.method public hidebysig static string
        Say(int32 n) cil managed
{
  // Code size       62 (0x3e)
  .maxstack  2
  .locals init ([0] bool 'is3の倍数', [1] bool 'is5の倍数')
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.0
  IL_0002:  bgt.s      IL_000a

  IL_0004:  newobj     instance void [mscorlib]System.ArgumentOutOfRangeException::.ctor()
  IL_0009:  throw

  IL_000a:  ldarg.0
  IL_000b:  ldc.i4.3
  IL_000c:  rem
  IL_000d:  ldc.i4.0
  IL_000e:  ceq
  IL_0010:  stloc.0
  IL_0011:  ldarg.0
  IL_0012:  ldc.i4.5
  IL_0013:  rem
  IL_0014:  ldc.i4.0
  IL_0015:  ceq
  IL_0017:  stloc.1
  IL_0018:  ldloc.0
  IL_0019:  brfalse.s  IL_0024

  IL_001b:  ldloc.1
  IL_001c:  brfalse.s  IL_0024

  IL_001e:  ldstr      "FizzBuzz"
  IL_0023:  ret

  IL_0024:  ldloc.0
  IL_0025:  brfalse.s  IL_002d

  IL_0027:  ldstr      "Fizz"
  IL_002c:  ret

  IL_002d:  ldloc.1
  IL_002e:  brfalse.s  IL_0036

  IL_0030:  ldstr      "Buzz"
  IL_0035:  ret

  IL_0036:  ldarga.s   n
  IL_0038:  call       instance string [mscorlib]System.Int32::ToString()
  IL_003d:  ret
} // end of method FizzBuzzer::Say

Andchin バージョンよりわずかに Code size が小さいですが、 それだけではありません。 Andchin バージョン と見比べてみると、 [mscorlib]System.… というものが入った長ったらしい行の数も少ないことが分かります。 これは .NET Framework の BCL (ベース クラス ライブラリ) の呼び出し部分です。 MSIL 上は 1行でも、 呼び出した BCL では何行にも渡る処理が行われているわけですね。

いかがでしょう。
短いコードが必ずしも少ない計算量になるとは限らない、ということをあらためて認識していただけたかと思います。

コードを短くすることに情熱を傾けるのは、 それがゲームであるなら、 楽しいし挑戦しがいもあります。 はっきり言って、 面白いです。
しかし、 製品コードを書くときは、 それが本当に計算量を減らしているのか。 あるいは、 読みやすく理解しやすくなっているのか、 変更しやすくなっているのか、 考えてみるべきでありましょう。 そして TDD 的には、 計算量がある程度増大したとしても、 理解しやすく変更しやすいコードのほうに価値を見出しています。 (だから、 リファクタリングのときに計算量は気にしない。 実際にパフォーマンスが出なかったらそのとき考える、 というスタンスを取ります。)
しかも面白いことに、 重複とネストを排除しただけの今回のような愚直なコードのほうが、 計算量が少なかったりするのです。

|

« [コラム] リファクタリングとは、 最適化することじゃない。 理解しやすくすること。 | トップページ | [コラム] リファクタリングはどっちへ進むべきだろう? (続々) »

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

コメント

ご丁寧な解説、ありがとうございます > あんどちん

おかげで分かったような気がします。
すごく大雑把に書くと、
・同じ結果が得られるのなら、 文字列連結しようが、 あるいはさらに他の方法で行おうが、 優劣は無い (どれかを選ぶことはできない)。 (←実行効率とかを言い出せば、また違うのは当然として。)
・しかし、全体のコード長は、文字列連結によって短くなる。よって、文字列連結を選ぶことができる。
…という 2段階があるのかなぁ?

私は、前段ですでに違いがあると思ってるので、まるっきりスレ違うようです。
そのあたりを、 [コラム] リファクタリングはどっちへ進むべきだろう? (了) に書いてみました。

投稿: biac | 2010年10月25日 (月) 22時23分

> 「なぜ文字列連結をするのか? 連結してよいと考えられるのは何故か?」という疑問です。
他の方はともかく僕がそう考えた理由を。最初に結論を書くと、
「入力に対する出力」が要求と判断した場合「それで求められる正しい出力と同じものを得ることができる」
からですね。
ここでポイントは
『「入力に対する出力」が要求と判断した』
を是とするか非とするかだと思います。

FizzBuzzより単純化して、例えば(適切でないかもしれませんが)
・数値nとmが入力される
・nをm回足した値を出力とする
という命題があった場合、言葉通りの実装なら
int result = 0;
for(int i = 0; i < m; ++i) result += n;
となりますね。しかし、同じ結果を得たいのであれば
result = n * m;
でいいわけですし、恐らくほとんどの人が下の処理で書くと思います。これは数値は掛け算で計算できることを知っているからですね。
では入力nを数値ではなく文字列とし、m回連結を行うとした場合はどうでしょう?下の処理では対応出来ませんが、上の処理はresultをstringにするだけで処理が実現できますね。そしてほとんどの人が上の処理の書き方で書くと思います。

僕の場合「入力」に対する「出力」*のみに*着目し、且つ「短い処理の方が良い」というスリコミからFizzBuzzを文字列連結で書くという発想に至っています。
例えばFizzBuzzを出力する条件が更に複雑化され「3と5と7の倍数ならばFizzBuzzを出力」となれば、文字列連結で書くことができるにも関わらずそれで書く人は減ると思います。この場合3と5の倍数判断と別に7の倍数判断が追加され、もし文字列連結で書くなら7の倍数且つ3の倍数であれば"Buzz"を連結するとなるので、連結で書いたほうが複雑になるからです。

あくまでも僕個人の考え方ですが、こんな感じの返答でいいでしょうか?

投稿: あんどちん | 2010年10月23日 (土) 11時48分

こんどこそ、 できました!!
124文字です! (^^)

class p{static void Main(){for(int n=0;n<100;)System.Console.WriteLine(++n%3<1?n%5<1?"FizzBuzz":"Fizz":n%5<1?"Buzz":n+"");}}

int 使うのに、 using System; は無くてよかったのね f(^^;
つまり、 Console 一箇所に System が必要なだけなので、 そこに付ければ "using " が削れるのでした。
それと、 剰余の結果はゼロまたは正の整数ですから、 その判定 "==0" は、 "<1" と書いても同じで、 それで 3文字削減。

三項演算子で、 {条件式} ? "文字" : n って書こうとすると、 C# のコンパイラは 「結果が文字になるか数値になるか分かんねぇじゃねーか!」 と怒るので、 普通は n.ToString() するんですけど、 文字列連結 (ぁあ、 出てしまった f(^^; ) させれば、 暗黙のうちに ToString() を呼び出してくれるので、 そこでも削減。
※ n+"" は、 String.Concat(Object, Object) の呼び出しに変換されます。その中で n.ToString() が実行されるというわけ。

このコードなら、 プラス 1文字で "Fizz Buzz" も返せるのだけど。
これ以上は、 さすがに短縮できんだろうなぁ。
ロジックを変えて、 全体を文字列連結だとすれば、 もう少し短くできそう。 (でも、 それすると "Fizz Buzz" 返してと言われたときに対応できない。)

ともあれ、 C# でも FizzBuzz の tDD (twitter Driven Development) 出来ると分かったので、 このネタもこれで終わり f(^^;

投稿: biac | 2010年10月23日 (土) 10時25分

まだ語っていないことがあって。
それは、「なぜ文字列連結をするのか? 連結してよいと考えられるのは何故か?」という疑問です。
ぼちぼちぐぐってみてたりしてますが、わかりません。
※ 可読性・変更容易性を無視してコードを短縮するため、 っていうのは分かります。 短くするぞ、 と言っていないのに、 文字列連結してる記事が少なからずあります。

理由が私には分からないものの、 FizzBuzz は文字列連結するもの、 という 「風潮」 みたいなものがあるようですね。
それをちょっと後押しする結果をもたらしてしまった、 というのが、 あんどちんのセッションで起きたことだったかなぁと思ってます。

でもほんと、 そういう 「副作用」 ってしょうがないというか、 避けられないですよね。
分かるはずだからとエラー処理を省いたサンプルコードを提示したらそのまま使われて、 テストでバグりまくりなんてしょっちゅうだもんねぇ… orz

投稿: biac | 2010年10月23日 (土) 10時00分

> あんどちんの影響力は大きいっすよ~。
自分的にはですが、本当にそんな風には思えないです。わんくまなら僕より優秀な人なんてゴロゴロいるわけだし。
今回に関しては「TDDの前に」「短いコードで例を示した」というのが他の方の考え方に影響を与えてしまったかもしれませんね。
取り敢えず(今後やる気は無いですが)セッションをやるときには他の方の資料に目を通し、内容がカブらないように心がけねばならないのかもしれませんね。
# しかし…そんなつもりがなくても他の方の考え方に影響を与えてしまうことがあるんですね。

恐らく午前中に同じ処理のロジックを見れば、多少なりともその日のセッションに対する考え方に影響を及ぼす可能性があるということがわかりましたから、「誰が」とかではなく敢えて誰かのセッションで解答を示した後別の方のセッションで同じ題材を取り上げれば今回と同じようになったと思います。
勉強会ならではのハプニングということで話を勝手に纏めたい気分です^^

投稿: あんどちん | 2010年10月23日 (土) 02時20分

やった! FizzBuzz 128文字♪

class p{static void Main(){for(int n=0;n<100;)Console.WriteLine(++n%3==0?n%5==0?"FizzBuzz":"Fizz":n%5==0?"Buzz":n.ToString());}}

もっとも、これも嘘ついてて f(^^;

C# は、必ず
using System;
は書かなきゃならんのよね~。
これが +13文字あるんで、 合わせて 141文字。
あと 1文字~ orz

投稿: biac | 2010年10月22日 (金) 21時20分

> 正直あそこで出したロジックが他の人の考えに影響を及ぼすとか考えてなかったです。

あんどちんの影響力は大きいっすよ~。
少なくともわんくまの中ではトップクラスでしょう。

# ブログのアクセス数 (ここは200/日くらい) も、 きっと比較にならんはず f(^^;

投稿: biac | 2010年10月22日 (金) 20時50分

僕のところのコメントから行ってもらうとCで82文字バージョンもあります。
それは兎も角…元々僕のセッションはFizzBuzzを最適化する話なんかじゃなくて、FizzBuzzをC++で書くとどう書けるか?と言うのが主題で、
・forで普通に書いたら
・for_eachで書いたら
・ラムダ式を使ったら
等々を言いたかっただけです。だからそれぞれでロジックは同じでした。

正直あそこで出したロジックが他の人の考えに影響を及ぼすとか考えてなかったです。辛辣な意見を言わせてもらえれば
「FizzBuzzのロジックくらい自分で考えられないの?」
って感じです。
# そのおかげでここで槍玉に上げられる羽目になってますしね

上にリンクを貼っていただいたとおり僕でも複数のパターンを考えられるんですけどねぇ…
まぁアリモノ使った方が楽だってのはあるんですが

投稿: あんどちん | 2010年10月22日 (金) 01時28分

> C# で tDD (twitter Driven Development - 140文字以内で書く! w ) が出来るとは思ってなかった。

ごめんなさい、大嘘でした m(_`_)m

ループ無い orz
クラス無い orz
コンパイルできまっしぇん orz

Gwpバージョンで 165文字
class p{static void Main(){for(int n=1;n<101;n++){Func<int,bool>f=(x)=>n%x==0;Console.WriteLine((f(3)?"Fizz":"")+(f(5)?"Buzz":"")+(!f(3)&&!f(5)?n.ToString():""));}}}

私ので 142文字
class p{static void Main(){for(int n=1;n<101;n++){bool f=n%3==0,b=n%5==0;Console.WriteLine(f&&b?"FizzBuzz":f?"Fizz":b?"Buzz":n.ToString());}}}

やぱし tDD できませぬ~ (;;)

投稿: biac | 2010年10月21日 (木) 23時45分

今回の課題で困ったことは、最初の命題が簡単すぎたことにより、どう書いたところで可読性が高すぎる点であると思います。また、拡張性に対する考慮も、仕様のキモが今ひとつ不確かなことから、現状の前提条件では議論に成り得ません。

さて、拡張仕様に対する私の認識は以下の追加ソースに書くことで説明します。
数字の末尾に7がきた場合にはSevenとする。条件が重なったときには、3,5,7の判定基準の順で該当する文字列を連結表示するです。後者の仕様は、元の命題で明確化されていないので、どうすればいいのか現時点では悩まないこととします。

static string say(int n) {
    var 特定数 = new Dictionary<int, string>() { { 3, "Fizz" }, { 5, "Buzz" }, { 7, "Seven" } };
    Func<int, bool> Is特定数 = x => (x == 7) ? ((n + 3) % 10 == 0) : (n % x == 0);
    Func<bool> 数字を発言 = () => {
        foreach (var c in 特定数.Keys) if (Is特定数(c)) return false;
        return true;
    };
    Func<string> 特定数発言 = () => {
        var ret = new StringBuilder();
        foreach (var c in 特定数.Keys) if (Is特定数(c)) ret.Append(特定数[c]);
        return ret.ToString();
    };
 
    if (n <= 0) throw new ArgumentOutOfRangeException("n", n, "--");
    return 数字を発言() ? n.ToString() : 特定数発言();
}

投稿: おがたん(GWP) | 2010年10月21日 (木) 22時03分

> コードを短くすることに情熱を傾けるのは、 それがゲームであるなら、 楽しいし挑戦しがいもあります。 はっきり言って、 面白いです。

…と書いたからには、 補足を。 f(^^;

そういうことやるには、 C# は向いてない気がする。
おがたんのリンク式バージョンを書き下してみたけど、130文字くらい。
static void Main(){Func<int,bool>f=(x)=>n%x==0;Console.Write((f(3)?"Fizz":"")+(f(5)?"Buzz":"")+(!f(3)&&!f(5)?n.ToString():""));}
※ 試してないんで、コンパイル通らなかったらゴメン。

これが C/C++ だと、100文字未満の戦いになる。 一桁違うわけですよ f(^^;

次は、 ネタにさせてもらってる @andochin のブログから。

今更FizzBuzz
int main(){for(int i=1;i<101;++i)printf(&"%d\n\0Fizz\n\0FizzBuzz\n"["j0040n4004n040"[i%15]%16],i);}
※ 99文字

これは、 C のポインタと、 ポインタと配列の関係、 それと C の文字列のことをキッチリ理解していないと、 読むのも覚束ないっす f(^^;

C言語で;の無いFizzBuzz
int main(int i,char** n){if(i=1){while(printf(i%3&&i%5?"%d":"",i),printf(i%3?"":"Fizz"),puts(i%5?"":"Buzz"),++i-101){}}}
※ セミコロン禁止! などという変態縛りを入れても、120文字!!

こっちは、0 が FALSE だと分かってれば、 なんとか読めるかな。

# どっちも、 書いてみろと言われたら、 わたしゃ逃げ出す f(^^;

# しかし、書いてみるもんだね~。 C# で tDD (twitter Driven Development - 140文字以内で書く! w ) が出来るとは思ってなかった。

投稿: biac | 2010年10月21日 (木) 22時02分

Helpful blog, bookmarked the website with hopes to read more!

投稿: roclafamilia | 2010年10月21日 (木) 20時19分

> 本当の仕様とは何だったのか

FizzBuzz 問題の本質はなんなのか?
たぶんそこの認識が決定的に違うんでしょうね。

「ある数が、とある条件のとき、こう言う」

…という仕様であると、私は捉えています。
すこしプログラム寄りの言い方をすれば、

「入力値を、とある条件にしたがって分類し、 分類ごとに異なる処理をすること」

…となりますか。

投稿: biac | 2010年10月21日 (木) 12時59分

まず最初に説明しておかないといけませんが、私のコードは、最初からFizzBuzz問題を1行で記述することを意図して書いたものではありません。本稿の命題の原型から整理した上で行き着いた先です。
行数が短ければ実ステップ数が短くなるだろうといった性能面への配慮をしたわけではなく、人に優しくすることを優先しています。したがって逆アセンブラの結果については、私からは議論する内容はありません。
また、短いコードをゲームとして追求して書いているつもりもありません。

さて、上記前提のもとでまず第一に気をつけているのがコードの量です。行数の増加はコード確認時にスクロール回数を多くし、結果コードの見通しを悪くすることにつながります。同様の理由で縦方向だけではなく、横方向の長さについても短くする方針にします。
2人のプログラム行数を見ると、3行 対 7行という結果になります。if のあとの改行を定形として必須とするならば12行分のスペースをとることになります。この差は2倍~4倍となり、他関数が同様のボリュームとなった場合にソースの見通しの面で格段の差がつくこととなります。
人が認知できるサイズになるか否かで、同一パターンの抽出、処理内容の整理などの面で格段の差が生まれます。

第二に気を付けていることは、機械的にコードの短縮を行うことです。
頭を使って共通部分の抽出を行うのではなく、むしろパズル的に共通部分の抜き出しを行い、コードの短縮を進めています。重複部分の削除をすすめる上で、クラス間のメソッド移動なども半ば機械的に行うことができます。

第三に気を付けていることは、日本語で書かれた仕様書の内容と1対1に対応するコードにするのではなく、本当の仕様とは何だったのかを考えることです。簡潔なコードが示す記述により、思いがけない隠された真の仕様が見える局面があります。

biacさんのコードは仕様と対応した記述になっており理解しやすく拡張性、メンテナンス性もあると思われているかもしれません。私も2ヶ月前ならこのようなコードをメインで書いていました。
しかし、今だとif構文の複数ブロックからなる記述は書き直しを考えるスタンスでいます。

投稿: おがたん(GWP) | 2010年10月21日 (木) 03時40分

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

トラックバック


この記事へのトラックバック一覧です: [コラム] リファクタリングはどっちへ進むべきだろう? (続):

« [コラム] リファクタリングとは、 最適化することじゃない。 理解しやすくすること。 | トップページ | [コラム] リファクタリングはどっちへ進むべきだろう? (続々) »