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

Ruby 2.5で発生する「プロを目指す人のためのRuby入門」との差異について

More than 1 year has passed since last update.

はじめに

この記事は書籍「プロを目指す人のためのRuby入門」に掲載できなかったトピックを著者自らが紹介するアドベントカレンダーの25日目(最終日)です。
本文に出てくる章番号や項番号は書籍の中で使われている番号です。

今回はRuby 2.5で発生する「プロを目指す人のためのRuby入門」との差異を紹介します。

必要な前提知識

「プロを目指す人のためのRuby入門」の第7章、第9章、第11章、第12章を読み終わっていること。

Ruby 2.5で発生する「プロを目指す人のためのRuby入門」との差異について

「プロを目指す人のためのRuby入門」(以下、本書)はRuby 2.4系を対象にして執筆されています。本書は紙の本なので、簡単に内容をアップデートすることができません。しかし、何もしないとどんどん内容が古くなってしまい、「なんか今使っているRubyと動きが違う」ということになってしまいます。

そこで新しいRubyのバージョンがリリースされて、本書の説明と異なる部分が出てきたときは、ネット上でその差異を説明するようにします。その説明を読めば、動きが違う部分があってもきっと落ち着いて対処できるはず、という算段です。

Ruby 2.5では以下の3点が本書の説明と異なる部分になります。

  • バックトレースの表示順が逆になる(9.2.1、11章、本書全般)
  • freezeしたオブジェクトに破壊的変更を加えようとするとFrozenErrorが発生する(7.8.1、7.8.2)
  • do/endブロック内でbegin/endなしのrescue/else/ensureが書けるようになった(9.6.7)
  • Bundlerが標準ライブラリに取り込まれた(12.8.2)

以下でそれぞれの内容を詳しく説明します。

バックトレースの表示順が逆になる(9.2.1、11章、本書全般)

Ruby 2.5ではエラー発生時のバックトレースの表示が逆になります。

以下はRuby 2.4のirbでわざとエラーを発生させたときの表示です。

$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]

$ irb
irb(main):001:0> a = 1 / 0
ZeroDivisionError: divided by 0
  from (irb):1:in `/'
  from (irb):1
  from /(Rubyがインストールされているパス)/irb:11:in `<main>'

一方、こちらはRuby 2.5のirbで実行した場合の表示です。

$ ruby -v
ruby 2.5.0dev (2017-12-24 trunk 61431) [x86_64-darwin16]

$ irb
irb(main):001:0> a = 1 / 0
Traceback (most recent call last):
        3: from /(Rubyがインストールされているパス)/irb:11:in `<main>'
        2: from (irb):1
        1: from (irb):1:in `/'
ZeroDivisionError (divided by 0)

ごらんのとおり、バックトレースの表示順がちょうど反対になっています(下に行くほど新しく、エラーに近い)。また、3: from ...のように、エラーが発生した箇所からいくつさかのぼったときの呼び出しなのかも連番で表示されるようになっています。

このような仕様変更があったため、9.2.1項のコードをRuby 2.5のirbで実行すると次のように表示されます。

irb(main):001:0> module Greeter
irb(main):002:1>   def hello
irb(main):003:2>     'hello'
irb(main):004:2>   end
irb(main):005:1> end
=> :hello
irb(main):006:0> greeter = Greeter.new
Traceback (most recent call last):
        2: from /(Rubyがインストールされているパス)/irb:11:in `<main>'
        1: from (irb):6
NoMethodError (undefined method `new' for Greeter:Module)

また、本書のサンプルコード内ではエラーが発生した場合の表示を簡略化して次のように書いていました。

greeter = Greeter.new
#=> NoMethodError: undefined method `new' for Greeter:Module

Ruby 2.4までは実際の表示でもメソッドの呼び出し(上のコードでいうところのgreeter = Greeter.new)とエラーメッセージの表示("NoMethodError"で始まる1行)が近かったのですが、Ruby 2.5になると先にバックトレースが表示されてから最後にエラーメッセージが表示されるため、メソッド呼び出しの行からエラーメッセージの行までが少し空いてしまいます。

Screen Shot 2017-12-25 at 5.38.42.png

メソッドを呼び出した直後に表示される"Traceback (most recent call last):"の文字列はエラーメッセージではなく、「トレースバック(最も直近の呼び出しが最後)」という意味の共通の文言ですので、これがエラーメッセージだと勘違いしないように注意してください。

ただし、従来どおりの順番でバックトレースが表示される場合もある

今回の仕様変更で少しややこしいのは、すべての状況においてバックトレースが逆順で表示されるとは限らない点です。バックトレースが逆順で表示されるのは以下の場合のみです。

  • irb上でコードを実行した場合
  • もしくは、組み込み定数であるSTDERRが変更されておらず、なおかつ、tty(標準入出力となっている端末デバイス)に出力されている場合

このような理由から、Ruby 2.5で実行しても「あれ、バックトレースの並び順が今までと同じだぞ?」というケースが起こりえます。たとえば、以下はMinitestからテストコードを実行している最中にエラーが発生した場合の出力例です。これはRuby 2.4でも2.5でも同じです(Minitest 5.10.3の場合。将来的にはMinitestの仕様が変わる可能性もあります)。

  1) Error:
GateTest#test_umeda_to_juso:
NameError: undefined local variable or method `distanse' for #<Gate:0x00007fdd851a9108 @name=:juso>
Did you mean?  distance
    /(プログラムのパス)/lib/gate.rb:36:in `calc_fare'
    /(プログラムのパス)/lib/gate.rb:26:in `exit'
    ./test/gate_test.rb:15:in `test_umeda_to_juso'

また、例外オブジェクトのbacktraceメソッドを呼び出したときも、内容や表示順はこれまでと同じです。以下は9.2.4項のサンプルコードをRuby 2.5で動かした場合ですが、出力内容は本書と変わりません。

エラークラス: ZeroDivisionError
エラーメッセージ: divided by 0
バックトレース -----
(irb):2:in `/'
(irb):2:in `irb_binding'
# 省略
/(Rubyがインストールされているパス)/irb:11:in `<main>'
-----

バックトレースの逆順表示はRuby 2.5の時点では「実験段階(experimental)」とのことです。将来的にはもっと適用範囲が広がる可能性はありますが、しばらくは従来の表示と逆順表示が混在することが予想されます。どちらの順番で表示されても落ち着いてエラー内容を確認できるようにしておきましょう。

freezeしたオブジェクトに破壊的変更を加えようとするとFrozenErrorが発生する(7.8.1、7.8.2)

freezeしたオブジェクトに破壊的変更を加えようとするとRuby 2.4まではRuntimeErrorが発生していました。

class Product
  DEFAULT_PRICE = 0
end

# クラスを凍結する
Product.freeze

# freezeすると変更できなくなる
Product::DEFAULT_PRICE = 5000
#=> RuntimeError: can't modify frozen #<Class:Product>

Ruby 2.5ではRuntimeErrorではなく、FrozenErrorが発生します。

Product::DEFAULT_PRICE = 5000
#=> FrozenError: can't modify frozen #<Class:Product>

よって、Ruby 2.5で動かす場合は、7.8.1項と7.8.2項のサンプルコードに登場するRuntimeErrorはすべてFrozenErrorに読み替えるようにしてください。

なお、FrozenErrorはRuntimeErrorクラスのサブクラスです。

FrozenError.superclass
#=> RuntimeError

do/endブロック内でbegin/endなしのrescue/else/ensureが書けるようになった(9.6.7)

9.6.7項ではメソッドの最初から最後までが例外処理の対象になっている場合はbeginとendが省略できる、と説明しました。

# メソッド内でbegin/endを省略して例外処理を書く
def fizz_buzz(n)
  if n % 15 == 0
    'Fizz Buzz'
  elsif n % 3 == 0
    'Fizz'
  elsif n % 5 == 0
    'Buzz'
  else
    n.to_s
  end
rescue => e
  puts "#{e.class} #{e.message}"
end

Ruby 2.5ではブロックの内部でも同じようにbeginとendが省略できます。たとえばRuby 2.4までは次のようにブロックの内部で必ずbeginとendを書く必要がありました。

users.each do |user|
  begin
    send_mail_to(user)
  rescue => e
    puts "#{e.class}: #{e.message}"
    puts e.backtrace
  end
end

ブロックの内部全体が例外処理の対象になっている場合、Ruby 2.5では次のようにbeginとendを省略できます。

# Ruby 2.5ではブロックの内部でbeginとendが省略できる
users.each do |user|
  send_mail_to(user)
rescue => e
  puts "#{e.class}: #{e.message}"
  puts e.backtrace
end

ただし、beginとendが省略できるのはdo/endでブロックを書いた場合のみです。{}を使ってブロックを書いた場合はこれまでどおりbeginとendを書く必要があります。

# {}を使ってブロックを書いた場合はbegin/endを省略できない
users.each { |user|
  send_mail_to(user)
rescue => e
  puts "#{e.class}: #{e.message}"
  puts e.backtrace
}
#=> SyntaxError: syntax error, unexpected keyword_rescue, expecting '}'
#   rescue => e
#   ^~~~~~

# {}を使う場合は従来どおりbeginとendが必要
users.each { |user|
  begin
    send_mail_to(user)
  rescue => e
    puts "#{e.class}: #{e.message}"
    puts e.backtrace
  end
}

Bundlerが標準ライブラリに取り込まれた(12.8.2)

Bundlerの標準添付は延期されたとのことです(参考)。

12.8.2項ではBundlerを使うために、gem install bundlerのようなコマンドでBundlerをインストールする必要があると書きました。ですが、Ruby 2.5ではBundlerが標準ライブラリとして取り込まれているため、別途インストールする必要がありません。

なお、本書ではBundler 1.15.1を対象にしていましたが、Ruby 2.5では1.16.1がインストールされます。バージョンが異なるため、bundle init後に作成されるGemfileの内容が若干異なったりするものの、基本的な使い方はどちらも変わらないため、ここでは特に説明は加えません。

その他、Ruby 2.5で導入される新機能

この記事では本書の説明と差異が発生するトピックだけをピックアップしましたが、Ruby 2.5ではこれ以外にも数多くの新機能が導入されます。Ruby 2.5についてもっと詳しく知りたい場合は以下の2つの記事を参照してください。

おわりに

「プロを目指す人のためのRuby入門・別館」アドベントカレンダーはこれで以上です!
長い間お付き合いいただきどうもありがとうございました。

「プロを目指す人のためのRuby入門」をすでにお持ちの方は、本書の内容と合わせて読むことで、さらに知識が深まったのではないでしょうか。

まだ持っていないという方はお近くの書店やAmazon等のネットショップでぜひ!
紙の本だけではなく電子書籍版もあります。

Ruby初心者の方は本書を読めば体系的にRubyに関する知識を学ぶことができるはずです。
本書に関する詳しい内容は以前書いたこちらのブログ記事をどうぞ。

みなさん、今後とも「プロを目指す人のためのRuby入門」をよろしくお願いします!:blush:

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
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
ユーザーは見つかりませんでした