Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
603
599

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rubyの凄く面白い特徴をRailsのコードを例に解説

Last updated at Posted at 2014-12-06

これはドリコムAdventCalendarの6日目です。
5日目の記事は、ドリコムの開発を支えるGitリポジトリ@gussanです。
7日目は、般若心経F*ck、コピペで徳を高める話@おーはらさんです。

自己紹介

ドリコムでアプリケーションエンジニアとしてネイティブゲームの開発を行ったりマネージメントをしたりしています。

その他の事はこちら参照:
https://gist.github.com/Shinya131/5d9e604d963177ee2cdc

Rubyの凄く面白い特徴をRailsのコードを例に解説

はじめに

この記事は、プログラミング言語Rubyが持つ凄く面白い特徴を、
Ruby on Rails の一部であるActiveSupport core extensionsのソースコードを題材に解説する物です。

題材に使うActiveSupportは、version 4.1です。

対象とする読者

この記事は、以下のような読者に役立つ内容を目指して書かれています。

  1. ここ最近rubyを使い始めたエンジニア (例えば新入社員)
  2. 普段rubyは使っていないもののrubyに興味があるエンジニア

記事の目的・ゴール

この記事を読んだ人がRubyのプログラミング言語としてのデザインや挙動に「おもしろさ・興味深さ」を感じてもらい、
さらなる学習のきっかけになる事を目的としてます。

あわよくば、この記事を読んだ人が、後に紹介する資料や書籍に興味を持って、
さらなる学習に着手してくれる事がこの記事のゴールです。

記事の概要

rubyの凄く面白い特徴である以下の機能について解説します。

  1. オープンクラス
  2. すべてのデータがオブジェクト
  3. ダックタイピング

今はこれらの単語に馴染みがなくても戸惑う必要はありません。
これらの単語が持つ意味と効果について解説するのがこの記事の主題です。

逆に言うとこれらのコンセプトについて理解している人にとっては物足りない内容と思われます;)

解説には、Ruby on Railsの一部であるActiveSupport core extensionsと呼ばれるライブラリのソースコードを題材に使います。
このライブラリはシンプルですが、上に挙げたRubyの面白い特徴がフルに活用されています。

第一章:オープンクラス

それでは、最初にオープンクラスについて解説していきます。
オープンクラスとは一言で言いうと「すでに定義されたクラスの振る舞いを、後から変える事ができる機能」です。

具体的にはどういう事でしょうか?
例を示します。


クラスAを定義します。
このクラスは#oneというメソッドを持っています。それ以外に特にメソッドはありません。

class A
  def one
    1
  end
end

Aのインスタンスを生成し#one実行します。1が返ってきます。
次に#twoを実行します。#twoなどというメソッドは無いので当然エラーになります。

class A
  def one
    1
  end
end

a = A.new
a.one #=> 1
a.two # NoMethodError: undefined method `two'

では次のように書いたらどうなるでしょうか?
一般的なプログラミング言語の場合すでに定義されているclassであるAを再度定義しようとしたといった感じのエラーになりそうですが、
rubyの場合はエラーになりません。

では、どのような挙動になるかというと、Aのインスタンスが#one#two両方のメソッドに応答できるようになります。

class A
  def one
    1
  end
end

class A
  def two
    2
  end
end

a = A.new
a.one #=> 1
a.two #=> 2

なぜこうなるのでしょうか?
大変ざっくりした言い方をしてしまうと、このように書いた場合...

class A
  def one
    1
  end
end

class A
  def two
    2
  end
end

あたかもこのような定義を行ったような挙動を示すのです。

class A
  def one
    1
  end

  def two
    2
  end
end

このように、一度定義されたクラスの振る舞いを、後から変える事ができます

オープンクラスという特性を使うと、メソッドの追加以外にも様々な挙動の変更ができます。
しかし、今回はメソッドの追加に絞って説明します。

オープンクラスの使用例

先ほど、新たに定義したクラスAに対してオープンクラスを使って挙動の変更を行いました。
このような変更は、全てのクラスに対して行う事ができます。

組み込みクラスである Array, Hash, Numeric, Stringなどのクラスも例外ではありません。

class String
  def one
    1
  end
end

"文字列".one #=> 1 # `String`にメソッドを追加。もちろんこのような拡張は不適切です(;´Д`)

ActiveSupport core extensions

さて、ではここで、ActiveSupport core extensionsによるStringに対する拡張の1つを題材に
オープンクラスの活用例を見て行きましょう。

そもそもActiveSupport core extensionsとは何なのでしょうか?
これはWebアプリケーションフレームワークであるRuby on Railsを構成するライブラリの1つです。

このライブラリは、rubyの各種組み込みクラスに対して、さまざまな拡張を行います。
拡張の目的は、Webサービスを開発するときに良く行われる処理を、よりスムーズに行えるようにする事です。

Stringに対する拡張: #squish

ActiveSupport core extensionsによってStringに追加される#squishを例にオープンクラスについて解説します。

このメソッドは文字列の冒頭と末尾についた空白を削除します。
また、複数の連続した空白を1文字にまとめます。

例:

# ActiveSupport core extensionsの一部を読み込んで、その機能を有効にしています。
# これ以降の例では省略します。
require 'active_support/core_ext/string/filters.rb'

# Stringに対して、`#squish`を呼び出します。
" \n  foo\n\r \t bar \n".squish
# => "foo bar" # 冒頭と末尾の空白が消える。複数の空白が1文字にまとめられる。

ユーザーが入力した文字を処理する時などに活躍しそうですね。


では、このメソッドがどのように実装されているか見て行きましょう。

おもむろにStringがオープンされメソッドが追加されています。
まずは、#squish!の挙動から読んだ方が分かりやすいでしょう。
これは、#squishの破壊的操作版です。

# active_support/core_ext/string/filters.rb
class String
  def squish
    dup.squish!
  end

  def squish!
    gsub!(/\A[[:space:]]+/, '')
    gsub!(/[[:space:]]+\z/, '')
    gsub!(/[[:space:]]+/, ' ')
    self
  end
end

rubyの場合、レシーバーを明示せずにメソッドを呼び出すとselfをレシーバーとしてメソッドが実行されます。
つまり#squish!の中で実行されている#gsub!self.gsub!と等価です。

そのため、文字列自身に対して#gsub!が呼ばれます。
文字列自身を破壊的に変更した後、selfreturnしています。

#squishの場合は、selfをコピーした物に対して#suquish!を実行して返却しています。

Stringクラスをオープンして機能が拡張されている様子が分かると思います。

本章のまとめ

このようにオープンクラスの機能を使うと、すでに定義されたクラスの振る舞いを変更する事ができます。
この機能はrubyの組み込みクラス群に対しても使えます。

これだけだでも面白いですが、これから紹介する2つの特徴と合わせて使うと更に面白い事が出来ます。

第2章:すべてのデータがオブジェクト

さて、2章の本題に入る前に少し寄り道をします。
この章の理解に役立つrubyの組み込み機能についての解説です。

準備: 本章の理解に役立つツール

rubyの事をrubyに聞く

次のコードを見てください。

1.class #=> Fixnum

Fixnumのインスタンスに対して、#classというメソッドを呼び出しています。
これは、レシーバーとなったインスタンスのクラスを取得するメソッドです。
すべてのインスタンスに対して使う事が出来ます。  


さらにこのような事ができます。
1#classの戻り値であるFixnumに対して#superclassというメソッドを呼び出しています。
これは、あるクラスの親クラスを取得するメソッドです。

1.class #=> Fixnum
1.class.superclass #=> Integer

rubyの場合、プログラムがプログラム自身の情報を豊富に保持しており、簡単に取得する事ができます。
以上で準備は終わりです。

本章では、これらのメタ情報を見ながら、「すべてのデータがオブジェクト」という事の意味について解説していきます。

すべてのデータがオブジェクト

rubyにおいてはすべてのデータがオブジェクトです。
メソッドを持たないただの値が無いのです。

例を見て行きましょう

'a'.class  #=> String    # 文字列は文字列型ではありません。Stringクラスのインスタンスです。
1.class    #=> Fixnum    # 数値は数値型ではありません。Fixnumクラスのインスタンスです

どのデータも、クラスのインスタンスです。
だから#classメソッドでクラスを取得する事ができます。


次の例を見てみましょう。
rubyではすべてのデータがオブジェクトです。
そして、rubyの記述のかなりの部分は、インスタンスに対するメソッドの呼び出しです。

# これは1(Fixnumのインスタンス)の`#+`メソッドの呼び出しです。2はメソッドの引数です。
1 + 2   #=> 3

# だからこのように書く事もできます。むしろ、上の書き方がシンタックスシュガーなのです。
1.+(2)  #=> 3

このようなデータもインスタンスです。 

nil.class  #=> NilClass  # nilはNilClassクラスのインスタンスです。
true.class #=> TrueClass # trueはTrueClassクラスのインスタンスです。
false.class#=> FalseClass# falseはFalseClassクラスのインスタンスです。

さらに特徴的な挙動を紹介します。

ここにStringクラスがあります。
具体的な文字列では無くクラスです。
#newすれば具体的なインスタンスを産みだす事ができますが、このままではただのクラスです。

String
String.new('a') #=> 'a'

さて、このStringクラスですが、このStringクラスにも「すべてのデータがオブジェクト」という法則が当てはまります。
Stringクラスはオブジェクト なのです。

クラスがオブジェクトとはどういう事でしょうか?
確かめてみます。

String.class #=> Class

クラスであるStringもまた、Classクラスのインスタンスなのです
rubyにおけるクラスとは、Classクラスのインスタンスなのです


「rubyにおけるクラスとは、Classクラスのインスタンス」
この特徴を使うことで、
ちょっと面白いことが出来ます。

Classに対して#newを行うと何が帰ってくるでしょうか?
もちろんClassクラスのインスタンスです。 つまり、通常のクラスです。

なので、このような書き方でクラスを定義する事ができます。

A = Class.new

これは、この書き方と等価です。

class A
end

Classクラスにも親がいる

先ほど登場したClassクラスですが、このクラスにも親クラスがあります。

Class.superclass #=> Module

はい、見慣れた物が出てきました。Moduleです。実は、ClassModuleを継承していたのです。
これはこれで大変興味深い部分なのですが、本題と関係ないので無視します。

さらにModuleの親クラスも調べます。

Module.superclass #=> Object

出ました。本章で最も重要な概念であるObjectです。

ここまで見てきたように、rubyにおいて全てのデータはインスタンスです。
そして、全てのインスタンスの元となっているクラス群は、このObjectを継承しています。

Objectにはすべてのインスタンスで共通して使われるメソッドが含まれます。
例えばインスタンスのコピーを行う#dupメソッドなどは、Objectに含まれます。

このObjectを継承することで、すべてのインスタンスで、Objectに定義されたメソッドを使う事ができます。


ここまで、rubyの重要な特徴について「すべてのデータがオブジェクト」と言ってきましたが少し言い換えます。

より重要なのは、
すべてのデータがインスタンスであり、すべてのインスタンスのクラスがObjectを継承しているため、
Objectの振る舞いを変更することで、すべてのインスタンスの振る舞いを変更できる」
という点なのです。

Objectの振る舞いを変更できる」の活用例

さて、先ほど上げた特徴ですが、どのように便利なのでしょうか?
ここでActiveSupport core extensionsに定義されている#tryメソッドを例に見て行きましょう。

#tryメソッド

#try#sendに似たメソッドです。

# tryとは関係なく普通に`#next`メソッドを呼び出す。
@number = 1
@number.next #=> 2

# tryを使って`#next`メソッドを呼び出します。
@number.try(:next) #=> 2 # メソッド名をsymbolとして渡すと、そのメソッドが実行される。

これだけだと#sendと何が違うのか分かりません。
ここからが#sendと違う所です。

# もしnilが入ったら?
@number = nil
#@number.next #=> NoMethodError: undefined method `next' for nil:NilClass # 普通にしたらエラー

@number.try(:next) #=> nil # レシーバーがnilだとnilが返る。

Fixnumが入るんだけど、nilが入る場合もある変数に対する処理」みたいな物を書く場合に便利です。

この#tryメソッドはObjectをオープンして追加されているメソッドです。
そのため、あらゆるインスタンスが#tryに応答する事ができます。

# どのインスタンスにも`#try`メソッドがある。
'1'.try(:to_i)          #=> 1
1.try(:to_s)            #=> "1"
[1,2,3].try(:pop)       #=> 3
{a: 1, b: 2}.try(:keys) #=> [:a, :b]

# nilも`#try`メソッドを持っている。何を渡してもnilが返る
nil.try(:to_i) #=> nil
nil.try(:to_s) #=> nil
nil.try(:pop)  #=> nil
nil.try(:keys) #=> nil

さらに、新たに定義された未知のクラスにたいしても、この振る舞いが追加されています。

class A
  def one
    1
  end
end

A.new.try(:one) #=> 1

#tryの実装

先ほどまでに解説したtryの実装を見て行きましょう。

実物の#tryの実装は、本章のテーマと関わらない部分も多いので、今回は実際の実装では無く大幅に抜粋したコードを使って解説します。

class Object
  def try(method_name)
    public_send(method_name)
  end
end

class NillClass
  def try(method_name)
    nil
  end
end

本当は、もっと様々な処理がありますが、根本的にはこれだけです。
たったこれだけのコードで、この章で例に挙げた#tryの使用例をすべて実現できます。

処理内容としては、以下のようになっています。

  1. Objectを変更によって、すべてのインスタンスにtryを定義。
  2. NillClassについてだけさらに#tryを再定義。Objectで定義された#tryは、NilClassに定義されたメソッドにオーバーライトされ、nilだけ独自の挙動を行う。

これも、

  • オープンクラス: すべてのクラスは定義後でも振る舞いを変えられる
  • すべてのデータがオブジェクト: Objectの振る舞いを変えることで、すべてのインスタンスの振る舞いを変えられる

というrubyの特徴のおかげです。

本章のまとめ

  • Objectの振る舞いを変えることで、すべてのインスタンスの振る舞いを変えられる。
  • さらに、継承とオーバーライトを利用して、特定のクラスのインスタンスだけ振る舞いを変える事ができる。

組わせて#tryのようなメソッドの実現が可能になっている。

第3章:ダックタイピング

最後にダックタイピングについて解説します。
これは、「あるインスタンスのクラスが何であろうと、必要なメソッドに応答さえ出来れば処理を行える」という特徴です。

Object#blank?

早速ですが例に入っていきます。
今回解説に使うのはActiveSuppor#blank?メソッドです。

これは「空っぽいインスタンスに対して呼び出すとtrueを返すメソッド」です。

[].blank?     #=> true # 空っぽい
{}.blank?     #=> true # 空っぽい

[1].blank?    #=> false
{a: 1}.blank? #=> false

''.blank?     #=> true # 空っぽい
'1'.blank?    #=> false

nil.blank?    #=> true # 空っぽい
true.blank?   #=> false
false.blank?  #=> true # 空っぽい

#blank?の実装

さて、多少端折っていますが、#blank?の実装は次の通りです。

class Object
  def blank?
    respond_to?(:empty?) ? !!empty? : !self
  end
end

class NilClass
  def blank?
    true
  end
end

class FalseClass
  def blank?
    true
  end
end

class TrueClass
  def blank?
    false
  end
end

class Array
  alias_method :blank?, :empty?
end

class Hash
  alias_method :blank?, :empty?
end

class String
  BLANK_RE = /\A[[:space:]]*\z/
  def blank?
    BLANK_RE === self
  end
end

class Numeric
  # No number is blank
  def blank?
    false
  end
end

基本的には前の章で見た、#tryの実装と似ています。

  • Objectの振る舞いを変えることで、すべてのインスタンスの振る舞いを変える
  • 継承とオーバーライトを利用して、特定のクラスのインスタンスだけ振る舞いを変える。

このような特徴を利用しています。

注目して欲しいのはこの部分です。

class Object
  def blank?
    respond_to?(:empty?) ? !!empty? : !self
  end
end

selfに対して、#respond_to?(:empty?)というメソッドを呼び出し、
結果がtrueなら#empty?の結果を#blank?の結果として返しています。

この#respond_to?とはどんなメソッドなのでしょうか?
これは、「あるインスタンスが、引数のメソッドに応答できるかどうかを返却するメソッド」です。

# レシーバーは#empty?に応答可能
[].respond_to?(:empty?)      #=> true
[1,2,3].respond_to?(:empty?) #=> true

''.respond_to?(:empty?)      #=> true
'a'.respond_to?(:empty?)     #=> true

# 実際呼び出せる
[].empty?      #=> true
[1,2,3].empty? #=> false

''.empty?      #=> true
'a'.empty?     #=> false

# レシーバーは#empty?に応答できない
1.respond_to?(:empty?) #=> false

# 実際呼び出せない
1.empty?               # NoMethodError

#blank?はこのrespond_to?を使う事によって、

  • #empty?に応答できるインスタンスの場合はそれを使う
  • 応答できないインスタンスの場合は、また別の処理を行う

という挙動を実現しているのです。

そのため、この#blank?は全く未知のクラスに対しても「#empty?に応答できるインスタンスの場合はそれを使う」という挙動を示すことが出来ます。

class EmptyClass
  def empty?
    true
  end
end

EmptyClass.new.empty? #=> true
EmptyClass.new.blank? #=> true

このように、rubyでは、
そのインスタンスのクラスの継承関係がどうなっていようと、ある特定のメソッドにさえ応答できれば、レシーバーとして扱えるようになっています。

この機能を「ダックタイピング」と呼び、以下のような言葉で比喩されます。

「もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルである」
(たとえ実際にはロボットだろうが、鴨だろうが)

本章のまとめ

  • rubyではインスタンスのクラスがどのような継承関係をもっていても、ある特定のメソッドに応答できさえすればレシーバーとして振る舞うような処理がかける。(ダックタイピング)

まとめ: 全体を通して

本記事では、冒頭で紹介rubyが持つ面白い3つの特徴について解説してきました。

オープンクラス

  • すでに定義されたクラスの振る舞いを、後から変える事ができる。
  • この機能は、rubyの組み込みクラス群を含めたすべてのクラスに対して使える。

すべてがObject

  • rubyにおけるすべてのデータはインスタンス。
  • そして、すべてのインスタンスのクラスはObjectを継承している。
  • そのため、Objectの振る舞いを変えることで、すべてのインスタンスの振る舞いを変更できる。

ダックタイピング

  • あるインスタンスのクラスがどのような継承関係をもっていても、
    ある特定のメソッドに応答できさえすればレシーバーとして振る舞うような処理が記述できる。

これらの3つの特徴は個々につかっても面白い事ができますが、組み合わせて使うことでより強力な機能を実現しています。
例えば、Active Support core extensionsは、
ライブラリがが実装された時点では、ライブラリの利用者がどのような継承ツリーを持った、どんなクラス群を作るか全く不明なのに、あたかも言語仕様自体を拡張したかのような機能追加を実現しています。

しかもこれが、言語仕様の拡張専用の文法では無く、普通のrubyコードの記述で実現されています。
この柔軟性は凄く面白く魅力的なものだと思います。


以上で解説は終わりです。
いかがでしたでしょうか? rubyに対して興味を深めて頂けましたか?

もし、興味を深めて頂いたようでしたら次のステップへ行きましょう。

次のステップ

書籍を読む

今回この記事で解説したような、内容がより詳細に、正確に、幅広く解説されている書籍です。

はじめてのRuby

メタプログラミングRuby

Rails Guidesを読む

例題として扱った、Active Supportの公式解説サイトです。
主要な機能が大変丁寧に解説されています。
http://railsguides.jp/active_support_core_extensions.html

また、このサイトの章立ては、Active Supportのソースコードとほとんど一対一で組み立てられており、Active Supportのソースコードをリーディングする時の資料として大変参考になります。

Active Support core extensionsの全てのソースコードはここから見ることが出来ます。
https://github.com/rails/rails/tree/master/activesupport

603
599
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
603
599

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?