give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

なぜRubyのcase/whenはインデントしないのかを考えてみた

はじめに

昨日はソニックガーデンにしては珍しく、ちょっとしたコーディングスタイル論争(?)が発生しました。
議論のネタになったのは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の影響を受けていると言うことです。
なるほどそうだったんですね〜。




なるほど、確かに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のパパ」ご本人に回答してもらえるとスッキリしますね。
どうもありがとうございました〜!


参考

Switch statement - Wikipedia
いろんな言語のswitch文が載ってます。