これはドリコム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
です。
対象とする読者
この記事は、以下のような読者に役立つ内容を目指して書かれています。
- ここ最近rubyを使い始めたエンジニア (例えば新入社員)
- 普段rubyは使っていないもののrubyに興味があるエンジニア
記事の目的・ゴール
この記事を読んだ人がRubyのプログラミング言語としてのデザインや挙動に「おもしろさ・興味深さ」を感じてもらい、
さらなる学習のきっかけになる事を目的としてます。
あわよくば、この記事を読んだ人が、後に紹介する資料や書籍に興味を持って、
さらなる学習に着手してくれる事がこの記事のゴールです。
記事の概要
rubyの凄く面白い特徴である以下の機能について解説します。
- オープンクラス
- すべてのデータがオブジェクト
- ダックタイピング
今はこれらの単語に馴染みがなくても戸惑う必要はありません。
これらの単語が持つ意味と効果について解説するのがこの記事の主題です。
逆に言うとこれらのコンセプトについて理解している人にとっては物足りない内容と思われます;)
解説には、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!
が呼ばれます。
文字列自身を破壊的に変更した後、self
をreturn
しています。
#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
です。実は、Class
はModule
を継承していたのです。
これはこれで大変興味深い部分なのですが、本題と関係ないので無視します。
さらに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
の使用例をすべて実現できます。
処理内容としては、以下のようになっています。
-
Object
を変更によって、すべてのインスタンスにtryを定義。 -
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