More than 1 year has passed since last update.

JavaScript や C ではちょっとした場面にぴりりと役に立つ三項演算子(条件演算子)だけれど、実は Ruby ではかなり存在感の微妙な要らない子だったりします。

ternary_sample.rb.js
a = 1
b = 2
x = "hi"
y = "bye"

v = a<b?x:y

こんなコードの場合を考へてみます。このコードは JavaScript 処理系では問題なく実行できるけど、 Ruby では Syntax Error であり、まったく動きません。

Ruby では、b? のようなメソッド名が許されて、 :y はシンボルです。また、 ?x は文字列の "x" と等価です。はてには、次のようなコードを書くこともできます。

arg_label.rb
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 の処理系は実際にどのように解釈してるのか、確認してみませう。

check_syntax_ternary.rb
#!/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}"

おのおのの手元で実行してみて欲しいのですが( irbpry でも動く)、実行結果は 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 で役に立つのか考へてみませう。

sample-use-if.c.js
if (foo()) {
    tmp = "fuga";
} else {
    tmp = "hoge";
}

bar(tmp);

このように foo() によって渡す引数を分岐したいときに、 tmp = を二回書くのは、激しく ださい ですね。そんなとき、三項演算子では if なしに次のやうに簡潔に書くことができます。

sample-use-ternary.c.js
tmp = foo()?"fuga":"hoge";
bar(tmp);

または直接このように書いて引数に渡すこともできます。

sample-use-ternary2.c.js
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 をご覧ください