JavaScript や C ではちょっとした場面にぴりりと役に立つ三項演算子(条件演算子)だけれど、実は Ruby ではかなり存在感の微妙な要らない子だったりします。
a = 1
b = 2
x = "hi"
y = "bye"
v = a<b?x:y
こんなコードの場合を考へてみます。このコードは JavaScript 処理系では問題なく実行できるけど、 Ruby では Syntax Error
であり、まったく動きません。
Ruby では、b?
のようなメソッド名が許されて、 :y
はシンボルです。また、 ?x
は文字列の "x"
と等価です。はてには、次のようなコードを書くこともできます。
def d (*arg)
arg
end
d 1, 2, 3, foo:"bar", fizz: :buzz, "hoge" => :fuga
# => [1, 2, 3, {:foo=>"bar", :fizz=>:buzz, "hoge"=>:fuga}]
そのような言語仕様のために、 Ruby の字句解析器・構文解析器は a<b?x:y
を正しいコードとしてパースできません。正しく意図通りに解釈させるには、 ?
と :
の前後にはきっちり空白を入れて、 a<b ? x : y
と書かなければいけないのです。
では Ruby の処理系は実際にどのように解釈してるのか、確認してみませう。
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'ripper'
require 'pp'
a = 1
b = 2
x = "hi"
y = "bye"
wrong = 'v = a<b?x:y'
puts "まちがひ: #{wrong}"
pp Ripper.tokenize wrong
pp Ripper.lex wrong
puts
correct = 'v = a<b ? x : y'
puts "せいかい: #{correct}"
pp Ripper.tokenize correct
pp Ripper.lex correct
puts
puts "eval: #{correct} #=> #{eval(correct).inspect}"
おのおのの手元で実行してみて欲しいのですが( irb
や pry
でも動く)、実行結果は ideone にも置いておきます。
さて、空白のない a<b?x:y
の部分を抜萃してみませう。
[[[1, 4], :on_ident, "a"],
[[1, 5], :on_op, "<"],
[[1, 6], :on_ident, "b?"],
[[1, 8], :on_label, "x:"],
[[1, 10], :on_ident, "y"]]
:on_ident
は識別子(メソッド名、変数名など)で、 :on_op
は演算子。ここで注目すべきは、 b?
と x:
がそれぞれひとつの塊として認識されてゐることです。 :on_label
は引数のラベルです。
つまり、この b?x:y
の部分は別の書き方をすると a < b?(:x => y)
と同じとして解釈されてることがわかります。それを証拠に、 def b? (*args); args; end
のようなコードを追加してみれば、エラーなく動作はすることがわかると思ひます。 (しかしそれでも a<b?x:y
は動かない)
さてここで、三項演算子がどうして C や JavaScript で役に立つのか考へてみませう。
if (foo()) {
tmp = "fuga";
} else {
tmp = "hoge";
}
bar(tmp);
このように foo()
によって渡す引数を分岐したいときに、 tmp =
を二回書くのは、激しく ださい ですね。そんなとき、三項演算子では if
なしに次のやうに簡潔に書くことができます。
tmp = foo()?"fuga":"hoge";
bar(tmp);
または直接このように書いて引数に渡すこともできます。
bar(foo()?"fuga":"hoge");
もし Ruby でも、たかだか2種類の分岐程度ならば三項演算子を使っても簡潔に書けます。しかし三項演算子では、それ以上の分岐が必要になったときには、ネスト地獄が待ってゐることを、みなさまは知ってゐることでせう……。
さて、 Ruby では以下のやうなコードが許されます。 (是非論はすこし脇においておいて)
tmp = if foo(); "fuga" else "hoge" end
bar tmp
# もちろんインデントしてもおk
tmp = if foo()
"fuga"
else
"hoge"
end
bar tmp
そして、この書きかたは case-when
でも使用することができます。
tmp = case foo
when :fizz
"fugafuga"
when :buzz
"piyopiyo"
else
"gudaguda"
end
bar tmp
分岐し放題ヤッター! (ちなみに公平のために、この記法は有名な 前田修吾さんのコーディング規約 では制限されてゐるものであることを紹介しておきます )
なほ、もしどうしても三項演算子を使用したいときは (a < b) ? x : y
のように条件式を ()
で括り、条件式中にもきちんと空白を入れ、きっちりと ?
と :
の前後を空けることをおすすめします。 (C や JavaScript でも条件式は ()
で括った方が読みやすいですね)
ちなみになのですが、 Ruby の三項演算子と if
は、見ためこそ異なりますが、インタプリタの内部的には まったく同じもの です。 parse.y をご覧ください 。