はじめに
昨日はソニックガーデンにしては珍しく、ちょっとしたコーディングスタイル論争(?)が発生しました。
議論のネタになったのはRubyのcase文のインデントについてです。
when節はインデントすべきか、それともcaseキーワードと揃えるべきかの議論になりました。
x = 1 # インデントする場合 case x when 1 puts "x is 1" when 2 puts "x is 2" else puts "x is other" end # インデントしない場合 case x when 1 puts "x is 1" when 2 puts "x is 2" else puts "x is other" end
Rubyのコーディング規約をいくつか見てみると、後者のインデントしないスタイルの方が多数派だったので、「インデントなしでいいじゃん」で結論付ければいいだけかもしれません。
ですが、そもそも「なんでインデントしないのさ?インデントすると何か不都合があるの?」というところがハッキリしないと、全員が納得しないと思います。
というわけで、もうちょっと理由を掘り下げてみることにしました。
C言語系のswitch/case文の場合
Java/C#/JavaScriptなど、C言語の構文に影響を受けている言語だとswitch文のcaseキーワードはインデントさせることが多いようです。
var x = 1; switch (x) { case 1: print("x is 1"); break; case 2: print("x is 2"); break; default: print("x is other"); }
こういう言語での開発経験がバックグランドにあると、インデントしないwhenに特に違和感を覚えるかもしれません。
また、Rubyでもインデントさせた方が視覚的にcase文の始まりと終わりを認識しやすいというメリットがあるようにも思えます。
x = 1 # 始まりと終わりがわかりやすいのはどっち?? case x when 1 puts "x is 1" when 2 puts "x is 2" else puts "x is other" end case x when 1 puts "x is 1" when 2 puts "x is 2" else puts "x is other" end
if/elsifと合わせるため?
もしかするとRubyではあえてインデントしない特別な理由があるのだろうかと思い、国内外のサイトを色々と調べてみました。
ネットで見かけた理由の一つは「case/whenとif/elsifは同じように使えるから、同じようなインデントにする」というものでした(参考)。
x = 1 # if/elsifを使った条件分岐 if x == 1 puts "x is 1" elsif x == 2 puts "x is 2" else puts "x is other" end # こんな風に書くとif/elsifとインデントの深さが揃う case when x == 1 puts "x is 1" when x == 2 puts "x is 2" else puts "x is other" end
でもまあ、この説明で腑に落ちるのか?って考えると、まだどうもしっくり来ません。。。
調べても考えてもよくわからない
他のサイトも色々見て回りましたが、これ!という確固たる理由は見当たりませんでした。
そこで自分なりに色々と理由を付けてみようとしましたが、結局これという理由は出てきませんでした。
あえて理由を挙げるなら「case + when 〜 end」というように、出だしのキーワードが必ず2つ出てくるのがややこしくなる原因なのかな、という気がします。
「if 〜 end」とか「begin 〜 end」みたいに、出だしのキーワードが一つで済むなら、インデントの考え方もハッキリすると思うんですけどね。
あと、Rubyのcase文はC言語系のswitch文とは別物(参考)なので、switch/case文の作法と照らし合わせて考えること自体がそもそもの間違いなのかもしれません。
インデントには厳しいPythonだと・・・
ところで、この問題を考えているときに「そうだ、インデントを文法的に強制するPythonはどう考えてるんだろう?」と思い、Pythonの構文をしらべてみました。
すると、驚くことに「Pythonにはswitch/case文にあたる構文は存在しません」となっていました・・・・!!(参考)
せっかく参考にしようと思ったのに、さらりとスルーされた気分ですorz
C言語の場合は・・・
さらにさらに、JavaやC#だけじゃなくてもっと遡ってC言語そのもののswitch/case文を見てみようと思い、K&Rの「The C Programming Language」を見てみました。
するとそこにはswitchとcaseが揃っているサンプルコードが載っていました。(参考: 3.4 Switch - PDF)
#include <stdio.h> main() /* count digits, white space, others */ { int c, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((c = getchar()) != EOF) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ndigit[c-'0']++; break; case ' ': case '\n': case '\t': nwhite++; break; default: nother++; break; } } printf("digits ="); for (i = 0; i < 10; i++) printf(" %d", ndigit[i]); printf(", white space = %d, other = %d\n", nwhite, nother); return 0; }
元祖C言語ではインデントしないんでしょうか?
Rubyのソースコードでは・・・
じゃあ、Rubyのソースコードもインデントが揃っているのか?と思い、GitHubのコードを見てみました。
適当にarray.cというコードを開いてみたのですが、ここではcaseがインデントされていました。
https://github.com/ruby/ruby/blob/trunk/array.c
VALUE rb_ary_aref(int argc, VALUE *argv, VALUE ary) { // ... switch (rb_range_beg_len(arg, &beg, &len, RARRAY_LEN(ary), 0)) { case Qfalse: break; case Qnil: return Qnil; default: return rb_ary_subseq(ary, beg, len); } return rb_ary_entry(ary, NUM2LONG(arg)); }
一方、mrubyのarray.cはswitchとcaseのインデントが揃っていました。
https://github.com/mruby/mruby/blob/master/src/array.c
mrb_value mrb_ary_aget(mrb_state *mrb, mrb_value self) { // ... switch(size) { case 0: return mrb_ary_ref(mrb, self, index); case 1: if (mrb_type(argv[0]) != MRB_TT_FIXNUM) { mrb_raise(mrb, E_TYPE_ERROR, "expected Fixnum"); } if (index < 0) index += a->len; if (index < 0 || a->len < (int)index) return mrb_nil_value(); len = mrb_fixnum(argv[0]); if (len < 0) return mrb_nil_value(); if (a->len == (int)index) return mrb_ary_new(mrb); if ((int)len > a->len - index) len = a->len - index; return ary_subseq(mrb, a, index, len); default: mrb_raise(mrb, E_ARGUMENT_ERROR, "wrong number of arguments"); } return mrb_nil_value(); /* dummy to avoid warning : not reach here */ }
なんだろう、同じRubyでも実装によってコーディングスタイルが異なるのでしょうか・・・?
まとめ(まとまらない)
というわけで調べれば調べるほど、だんだん訳がわからなくなってきました。
case/whenにせよ、switch/caseにせよ、「これが筋の通ったあるべきコーディングスタイルだ!」というものは存在せず、言語の慣習やプログラマの好みによって、インデントする、しないが変わってくるだけなのかもしれません。むむむむ。
Matz先生による回答 (2013.2.28 10:00am 追記)
Matz先生に回答をもらいました!Eiffelの影響を受けていると言うことです。
なるほどそうだったんですね〜。
@yukihiro_matz おはようございます。こんなブログ( http://t.co/EVpJ5yoSOI )を書いてみたのですが、caseとwhenを揃えた方がいい理由が何かあれば聞いてみたいです。
— Junichi Ito (伊藤淳一) (@jnchito) February 28, 2013
@jnchito RubyのインデントスタイルはEiffelの「櫛型構造」の影響を受けています。簡単に言うとif~elsif~else~endに合わせるということですね。のでcaseとwhenを揃えるのが好みです。
— Yukihiro Matsumoto (@yukihiro_matz) February 28, 2013
@yukihiro_matz なるほど、Eiffelの影響だったんですか!ちょっと意外でした。Eiffelにはあまり詳しくないですが、このあたり( http://t.co/7b2deC4jx9 )を見ると納得できますね。ご回答ありがとうございました!
— Junichi Ito (伊藤淳一) (@jnchito) February 28, 2013
なるほど、確かにRubyっぽいです。(正確にはRubyがEiffelっぽいのですが)
http://www.maths.tcd.ie/~odunlain/eiffel/eiffel_course/eforb.htm
inspect value + 1 when 1, 3, 5, 7, 8, 10, 12 then day.set_range (1, 31) when 2 then day.set_range (1, 28) -- leap? when 4, 6, 9, 11 then day.set_range (1, 30) end
また、Rubyのソースコードもあえてスタイルを使い分けているみたいです。
RubyのCソースのインデントスタイルはCRubyはBSDスタイル(改)、mrubyはGNUスタイル(改)です。
— Yukihiro Matsumoto (@yukihiro_matz) February 28, 2013
「Rubyのパパ」ご本人に回答してもらえるとスッキリしますね。
どうもありがとうございました〜!
参考
Switch statement - Wikipedia
いろんな言語のswitch文が載ってます。