はじめに

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

今回はRailsチュートリアルのサンプルコードを文法解析します。

必要な前提知識

「プロを目指す人のためのRuby入門」の最後(付録を含む)まで読み終わっていること。

Railsチュートリアルのサンプルコードを文法解析してみる

「プロを目指す人のためのRuby入門」の書籍ページには次のような紹介文を載せています。

本書の内容を理解すれば,開発の現場で必要とされるRuby関連の知識を一通り習得できます。そして,「今まで呪文のようにしか見えなかった不思議な構文」や「実はあまりよくわからないまま,見よう見まねで書いているコード」も自信をもって読み書きできるようになるはずです。

http://gihyo.jp/book/2017/978-4-7741-9397-7

そうなんです。「プロを目指す人のためのRuby入門」を読めば、自信をもってRubyのコードを読み書きできるようになります!

あ、そこのあなた!今「本当に~?」って思いましたね??

いいでしょう、それをこれから証明してみせます!

というわけで、この記事ではRuby on Railsチュートリアル(以下Railsチュートリアル)のサンプルコードを題材にして、「プロを目指す人のためのRuby入門」で学んだ知識で本当にRubyのコードが読めるようになるのかどうか確認していきます。

対象となるコード

Railsチュートリアル Rails 5.1版・第14章のサンプルコード

https://github.com/yasslab/sample_apps/tree/master/5_1_2/ch14

ただし、Viewやテストコードは今回対象外とします。

文法解析のフルバージョン

どのプログラムのどの要素が、「プロを目指す人のためのRuby入門」のどこで説明されているのかは、以下のpull requestのdiffを確認するとわかります。

https://github.com/JunichiIto/sample_apps/pull/1/files

diffでは以下のように簡単な文法説明と、その内容を詳しく説明している「プロを目指す人のためのRuby入門」内の項番号や掲載ページをコメントとして記入しています。

Screen Shot 2017-12-23 at 5.18.53.png

解説動画

diffを見るだけではピンと来ないと思うので、口頭で説明を入れた動画も作成しました。
こちらもあわせてご覧ください。
ただし、ちょっと長いので(37分)、1.5倍速ぐらいで見ることをオススメします。

https://youtu.be/FQyNF2LKguI

Screen Shot 2017-12-23 at 5.16.14.png

やや上級者向けのポイント10個

上のdiffや動画を全部チェックするのは大変だと思うので、その中からやや上級者向けのポイントを10個ピックアップして以下で説明していきます。

1. ||でnilでない方の値を返す

# request.referrerが偽(nilまたはfalse)ならroot_urlをredirect_toの引数とする(2.10.1)
redirect_to request.referrer || root_url

Rubyの||&&は毎回truefalseを返すとは限りません。そうではなく、真と偽が決定した時点でそのときの値を返すようになっています。なので、request.referrer || root_urlrequest.referrernil(またはfalse)でなければその値を、nilならroot_urlを返します(2.10.1項参照)。

ただ、上のような書き方だと一瞬redirect_to request.referrerroot_url||で比較しているようにも見えてしまうので、次のように()で囲んだ方が読み手に優しいと思います。

# ()を付けなくても結果は同じだが、こう書いた方が読み手に優しい
redirect_to(request.referrer || root_url)

2. 条件分岐で変数に値を代入する

# 条件分岐で変数に代入(コラム173ページ)
if (user_id = session[:user_id])
  @current_user ||= User.find_by(id: user_id)

2つの値が同じかどうかを確認するときは通常==を使いますが、上のコードのif文では==ではなく=が使われています。ここでやっているのは値の比較ではなく、変数への代入です。user_idに真の値(falseでもnilでもない値)が代入されたら条件分岐自体も真になります(コラム173ページ参照)。

なお、()は付けなくても同じように実行できます。

# ()は付けなくても動きは同じ
if user_id = session[:user_id]
  @current_user ||= User.find_by(id: user_id)

3. nilなら代入、nilでなければ何もしない

先ほどのコードの2行目にも注目してください。

@current_user ||= User.find_by(id: user_id)

||=を使うと、左辺の値がnil(またはfalse)である場合に右辺の値を代入します。左辺の値が真であれば何も代入しません(コラム174ページ参照)。

なので、上のコードは「@current_usernilなら(=まだ何も代入されていないなら)、指定されたuser_idのUserを代入しろ、そうでなければそのままでOK」の意味になります。

4. 二重コロンでメソッドを呼び出す

# 二重コロンを使ってメソッドを呼び出す(コラム324ページ)
gravatar_id  = Digest::MD5::hexdigest(user.email.downcase)

hexdigestの左にある二重コロン(::)に注目してください。オブジェクトのメソッドを呼び出すときは通常ドット(.)を使いますが、実は二重コロンを使って呼び出すこともできます(コラム324ページ参照)。

s = '123'
# ドットでメソッドを呼び出す
s.to_i  #=> 123
# 二重コロンでメソッドを呼び出す
s::to_i #=> 123

なので、Digest::MD5::hexdigestDigest::MD5.hexdigestと書いたことと同じになります。

5. クラス名.メソッド名の形でクラスメソッドを定義する

class User < ApplicationRecord
  # 省略

  # クラスメソッドの定義(def self.xxxを使わないパターン)(7.3.4, 7.10.8)
  def User.digest(string)
    # ...

「プロを目指す人のためのRuby入門」の中ではdef self.digest(string)のように、self.xxxの形式でクラスメソッドを定義していましたが、上のコードであれば「self = Userクラス」になるのでself.User.に置き換えても同じことになります(7.3.4項、7.10.8項参照)。

6. sendメソッドでメソッドを呼び出すメソッドを動的に切り替える

def authenticated?(attribute, token)
  # sendメソッドの利用(12.6)
  digest = self.send("#{attribute}_digest")

sendメソッドを使うと、文字列やシンボルを使って呼び出すメソッドを動的に指定できます(12.6節参照)。上のコードが何をやりたいのかピンと来ない人は、次のように書いた方がイメージが付きやすいかもしれません。

def authenticated?(attribute, token)
  digest =
    case attribute
    when :activation
      activation_digest
    when :reset
      reset_digest
    when :remember
      remember_digest
    else
      raise ArgumentError, attribute
    end

つまり引数のattributeの値と、xxx_digestメソッドのxxxの部分が同じになるので、self.send("#{attribute}_digest")のようにsendメソッドを使って引数をそのままメソッド名の一部に置き換えているわけです。

ただし、sendメソッドを多用しすぎるとコードの見通しが悪くなりますし、ユーザーの入力値をそのままsendメソッドに引き渡すような実装になっていると悪意のあるユーザーから任意のメソッドを実行されかねません。くれぐれも用法と用量をお守りください :hospital:

7. selfをメソッドの引数として渡す

class User < ApplicationRecord
  # 省略

  def send_activation_email
    # 自分自身(Userクラスのインスタンス)を引数として渡す(7.5)
    UserMailer.account_activation(self).deliver_now
  end

selfはRubyの文法上「擬似変数」(最初から定義されている特殊な変数で、再代入できないもの)になります(2.12.4項参照)。つまり、「一種の変数」と見なすことができるので、上のコードのようにselfをメソッドの引数として渡すことも可能です。selfについては7.5節で詳しく説明しています。

8. クラス構文の直下でif文のような処理を書く

class PictureUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  process resize_to_limit: [400, 400]

  # クラス構文の直下に処理を書く(7.5.2)
  if Rails.env.production?
    storage :fog
  else
   storage :file
  end

Rubyではクラス構文の直下にも自由にコードを書くことができます。クラス構文の直下に書いたコードはそのクラス定義が読み込まれる瞬間に上から順に実行されます(7.5.2項参照)。

上のコードはRailsの実行環境が本番環境であればstorage :fogを、そうでなければstorage :fileを実行(設定を切り替え)します。

9. 左辺に何もない二重コロンでトップレベルの定数を参照する

# 左辺に何もない二重コロンでトップレベルの定数を参照する(コラム308ページ)
config.log_formatter = ::Logger::Formatter.new

上のコードでは::Loggerの左側に何もありません。二重コロンの左辺に何もない場合は、必ずトップレベルの定数(定数にはクラスやモジュールも含まれる)が参照されます(コラム308ページ参照)。

つまり、上のコードであればRuby標準のLoggerクラスを参照していることになります。

10. 例外的に大文字で始まるメソッドを呼び出す

# 例外的に大文字で始まるメソッドを呼び出す(2.6節の備考欄)
# https://docs.ruby-lang.org/ja/latest/method/Kernel/m/Integer.html
workers Integer(ENV['WEB_CONCURRENCY'] || 2)

Rubyでは整数を表すクラスとしてIntegerクラスがあります。しかし、上のコードではInteger(...)のようにクラス名のうしろに丸括弧が付いています。これはいったい何をやっているのでしょうか?

実はこのIntegerはクラスではなく、KernelモジュールのIntegerメソッドです。Rubyのメソッドは通常小文字から始まりますが、まれに大文字で始まるものが存在します(2.6節の備考欄参照)。

Integerメソッドは引数で渡された値を整数に変換しようと試みます。変換できなかった場合はエラーが発生します。

# 出典: https://docs.ruby-lang.org/ja/latest/method/Kernel/m/Integer.html (一部抜粋)
p Integer(4)          #=> 4
p Integer(9.88)       #=> 9
p Integer(nil)        # can't convert nil into Integer (TypeError)
p Integer("hoge")     # `Integer': invalid value for Integer: "hoge" (ArgumentError)
p Integer("")         # `Integer': invalid value for Integer: "" (ArgumentError)

Integerメソッドの仕様についてはRubyの公式ドキュメントを参考にしてください。

https://docs.ruby-lang.org/ja/latest/method/Kernel/m/Integer.html

他にもあります!

いかがだったでしょうか?初心者の方はこうやってみると「まだまだ知らないポイントがたくさんあった」と感じるのではないでしょうか?

冒頭で紹介したpull requestや動画の中では、他にもこうしたポイントを紹介しています。全部チェックしたい方は以下のリンクを参照してください。

このようなRubyの文法や言語機能に関する知識を深めるためにも、ぜひ「プロを目指す人のためのRuby入門」を活用してください:blush:

次回予告

いよいよ次回で最終回です。次回はRuby 2.5で発生する「プロを目指す人のためのRuby入門」との差異を説明します。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.