Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
35
Help us understand the problem. What is going on with this article?
@tadsan

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 をご覧ください

35
Help us understand the problem. What is going on with this article?
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
tadsan
僕に警備する自宅をください。Emacs初心者。Rubyist。 全ての投稿された記事は別段の表記がない限りはCC 3.0 BY-SA https://creativecommons.org/licenses/by-sa/3.0/deed.ja で二次利用できます。 記事中に含まれる全てのコードスニペットの著作権は抛棄するので、煮るなり焼くなりお好きにどうぞ。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
35
Help us understand the problem. What is going on with this article?