Help us understand the problem. What is going on with this article?

Rubyの三項演算子の話

More than 5 years have 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 をご覧ください

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした