Ruby言語仕様、メタプログラミング【パーフェクトRuby】

GWでまとまった時間をとれているので、『パーフェクト Ruby』を久々に読み直しています。Rubyを深く勉強したいと思った時に、それに答えてくれるすばらしい本だとあらためて思いました!

この記事は「Part2言語仕様、Part3メタプログラミング」を読んでいる中で忘れたくない部分を備忘録メモしただけの記事です。

(05-10 17:30) メタプログラミング再勉強


Part2言語仕様

帯域脱出catch・throw構文

1
2
3
4
5
6
7
8
9
10
11
12
13
catch :triple_loop do
loop do
puts 'one'
loop do
puts 'two'
loop do
puts 'three'
throw :triple_loop
end
end
end
end
#=> one two three と出力して終了

Procオブジェクトをブロックとして渡す

最近ちゃんとしたプログラミングでProcやlambdaをはじめて使いました。処理の再利用性や可読性をうまく高めるコトができるのでオススメです!

1
2
3
4
5
6
7
people = %w(John Doe Geoge)
block = Proc.new { |name| puts "Hello #{name}!" }
people.each &block
#=> Hello John!
#=> Hello Doe!
#=> Hello Geoge!

Enumerable : reverse_each, each_slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 末尾から逆順に繰り返す
(1..3).reverse_each do |val|
puts val
end
#=> "3" "2" "1"
# 要素をn個ずつ区切って繰り返す
(1..5).each_slice(2) do |a, b|
p [a, b]
end
#=> [1, 2] [3, 4] [5, nil]
# ブロックの戻り値が真となる要素をすべて返す
p (1..5).to_a.select { |i| i.even? }
#=> [2, 4]
# selectの逆の動作をする
p (1..5).to_a.reject { |i| i.even? }
#=> [1, 3, 5]
# 畳み込み演算 inject
p (1..5).to_a.inject(:+) #=> 15

Part3メタプログラミング

class定義式とClass.new

Rubyでクラスを定義するためには、class式を使うのが一般的ですが、Class.newでもクラスを定義できます。この2つの定義方法の違いは次のようなソースで現れます。

1
2
3
4
5
6
7
8
9
external_scope = 1
class ExpDefineClass
puts external_scope # NoNameErrorが発生
end
NewDefineClass = Class.new do
puts external_scope #=> 1
end

Class.newでクラスを定義すると外側のスコープを参照できるので、動的にクラスを定義する場合に有用です。

クラス変数とクラスインスタンス変数

Rubyにはクラス変数(@@xxx)とクラスオブジェクトがもつインスタンス変数(@xxx)があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Klass
@class_instance_val = :class_instance_val
@@class_val = :class_val
def self.class_instance_val
@class_instance_val
end
def instance_method
puts @class_instance_val #=> nil
puts @@class_val #=> :class_val
end
end
Klass.class_instance_val #=> :class_instance_val
klass = Klass.new
klass.instance_method
#=> nil
#=> :class_val

クラス変数と、クラス・インスタンス変数の違いは次のとおり。

# クラス変数
* クラスに関わる全てで共通して使いたい情報を保持する
* 継承しても保持され続ける

# クラス・インスタンス変数
* そのクラスで閉じた情報を保持する
* 継承したクラスからは参照・変更ができない

メソッドの定義

モジュール#define_methodを使用すれば、メソッド定義式def xx; endを省略して定義できます。

1
2
3
4
5
class Klass
define_method :instance_method, -> { :instance_method }
end
object = Klass.new
object.instance_method #=> :instance_method

これを応用すると、呼び出されるメソッドの優先順位を変更できます。

* 通常は継承した際に最後にインクルードしたメソッドが呼ばれる
* Module#define_methodを使うことで、特定のメソッドのみ継承ツリーを無視して利用できる

例としては次のとおり。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module FirstIncludeModule
def same_name_method
:first_same_method
end
end
module SecondIncludeModule
def same_name_method
:second_same_method
end
end
class Klass
include FirstIncludeModule
include SecondIncludeModule
end
object = Klass.new
puts object.same_name_method # => :second_same_method
class Klass
include FirstIncludeModule
include SecondIncludeModule
define_method :same_name_method, FirstIncludeModule.instance_method(:same_name_method)
end
object = Klass.new
puts object.same_name_method #=> :first_same_method

特異クラスと特異メソッド

特異クラスとは特異メソッドが定義されているクラス。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OriginalClass
end
obj = OriginalClass.new
def obj.new_singleton_method
:new_singleton_method
end
puts obj.class.method_defined? :new_singleton_method
#=> false
puts obj.singleton_class.method_defined? :new_singleton_method
#=> true

特異メソッドの作成タイミングは次のとおり。

* 特異メソッドを定義するタイミング
* 特異クラスの定義式を評価したタイミング

nil, Fixnum, Symbolのインスタンスやtrue, false, nilオブジェクトは特異クラスが定義できないようになっています。(TypeErrorが発生する)

また、特異クラスはインスタンスを生成しようとしたり、サブクラスを作ろうとするとエラーが発生する。

特異クラスのオブジェクトにモジュールの機能を組み込む場合は、Object#extendを使う。

1
2
3
4
5
6
7
8
9
10
module ExtendedModule
def extend_method
:extend_method
end
end
obj = Object.new
obj.extend ExtendedModule
puts obj.extend_method #=> :extend_method

Rubyのメソッド探索順序

Rubyには特異メソッドをもつ特異クラスや、Mixinなどがありますが、それらを含むメソッド探索順序のまとめは次のとおりです。

(1) Rubyインタプリタはレシーバの特異クラスからメソッドを探す
(2) レシーバの直接のクラス(特異クラスの親クラス)からメソッドを探す
(3) 親クラスを参照してメソッドを探す。継承ツリーの一番上まで繰り返す
(4) 引数を『メソッド名、引数』にして method_missing を呼び出す

検索順序のサンプルソースはこちら。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module HelloModule
def hello
:hello_module
end
end
class GrandParentClass
def hello
:grand_parent_hello
end
end
class ParentClass < GrandParentClass
include HelloModule
end
class ChildClass < ParentClass
end
child = ChildClass.new
p child.hello #=> :hello_module

ちなみに『mod.ancestors』メソッドを使うと、継承関係を確認できます。

モジュール#prepend

Ruby 2。ゼロから追加されたメソッドで、モジュール#includeのようにクラスやモジュールに別のモジュールのメソッドを追加します。モジュール#includeとの違いは、『自分で定義したメソッドよりも組み込んだメソッドが優先される』点です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module PrependModule
def embeded_method
:prepended_module
end
end
class PrependedClass
prepend PrependModule
def embeded_method
:prepended_class
end
end
obj = PrependedClass.new
obj.embeded_method #=> :prepended_module

これによりRailsのbefore_actionなどのように、メソッドが呼び出される前に行う特定の処理を簡単に記述できます。

Refinements

まず、Rubyのクラスは「オープンクラス」と呼ばれており、自由に書き換えることができます(別名ではモンキーパッチと呼ばれます)。例としてはこちら。

1
2
3
4
5
6
7
class String
def good_night
puts "#{self} good night"
end
end
’John’.good_night #=> 'John good night'

そして、Ruby 2。ゼロからはRefinementsによって同様のことができるようになりました。ただし、Ruby 2.0では実験的な機能として、ファイル単位に限定されていました。Ruby 2。ゼロからはモジュール内でのみ特定のクラスのメソッドを書き換えることができるようになりました。

具体的な使い方の例としてはこちら。

1
2
3
4
5
6
7
8
9
10
module RefineModule
refine String do
def good_night
puts "#{self} good night"
end
end
end
using RefineModule
'John'.good_night #=> 'John good night'

モジュール#refineusingメソッドで使うことができます。

「#Ruby 2.1どこが新しい?」問題解説記事

メソッドの定義場所を探す

Method#source_locationを使うと、メソッドの定義場所を確認できます。

1
2
3
4
5
6
require 'csv'
csv = CSV.new('')
m = csv.method(:convert)
m.source_location
#=> ["/Users/komji/.rbenv/versions/2.1.0/lib/ruby/2.1.0/csv.rb", 1686]

オープンクラス

Rubyでは定義済のクラスに対して、再度メソッドを定義し直したり、メソッドを追加できます。たとえば、ゼロからselfまでの配列を返す、Numeric#stepsを作ります。

1
2
3
4
5
6
7
8
class Numeric
def steps
return [] if self <= 0
0.upto(self).to_a
end
end
p 8.steps #=> [0, 1, 2, 3, 4, 5, 6, 7, 8]

オープンクラスは気軽に使える反面、影響が大きく、意図していない動きをする可能性があります。そのため、使い方にはくれぐれも注意すべきですし、Refinementsなどで代替も要検討です!

BasicObject#method_missing

BasicObject#method_missingを使うと、定義されていないメソッドに対しても処理を行うことが可能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DelegateArray
def initialize
@array = []
end
def method_missing(name, *args)
# 特定のメソッドのみ上書きするようにする
if name == :<<
return @array.__send__ name, *args
end
super
end
end
da = DelegateArray.new
da << 1
p da

存在しないメソッドを#method_missing無いで存在しないメソッドを呼び出すと、SystemStackErrorが発生します。

Active Recordではmethod_missingをつかっているため、method_missingをオーバーライドすると予期せぬ動作をしすぎると思います。それを考えて、method_missingをオーバーライドする場合は、必要なメソッドのハンドリングのみ行い、superを呼び出すほうが行儀のよいプログラムといえます。

evalについて

eval入門: Rubyのyieldは羊の皮を被ったevalだ!

http://melborne.github.io/2008/08/12/Ruby-yield-eval/

evalのパートは単純な使い方は分かるんですが例題を読み進めるとかなり詰まってしまいました。ということで助けてもらったのが、『Rubyのyieldは羊の皮を被ったevalだ!』です。@merborneさん、いつもありがとうございます!

evalを使って動的にメソッドを定義する

eval(evaluate/評価の略)の整理。たとえば次のように動的にメソッドを定義する際にevalを使うと便利。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Sample
val_list = %w(first second third)
val_list.each do |val|
eval <<-EVL
attr_reader :#{val}, :before_#{val}
def #{val}=(val)
@before_#{val} = @#{val}
@#{val} = val
end
EVL
end
end
obj = Sample.new
obj.first = 10
puts obj.first #=> 10
obj.second = 20
puts obj.second #=> 20
obj.third = 30
puts obj.third #=> 30
obj.third = 40
puts obj.before_third #=> 30
puts obj.third #=> 40

Bindingオブジェクト

Rubyではあるコンテキストで定義された変数やメソッドをまとめたBindingオブジェクトが存在します。これを使うと次のようなことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class EvalTarget
def initialize
@initialize_val = 'init val'
end
def instance_binding
local_val = 'local val'
binding
end
private
def private_method
'private method'
end
end
target = EvalTarget.new
binding_obj = target.instance_binding
puts (eval '@initialize_val', binding_obj) #=> 'init val'
puts (eval 'local_val', binding_obj) #=> 'local val'
puts (eval 'private_method', binding_obj) #=> 'private method'

Procについて

Procオブジェクトとは『処理をオブジェクトとして抽象化したもの』だそうです。Procオブジェクトを使うとブロックをオブジェクトとして扱えます。

Procをcase文の条件式に当てはめる

Procの実用例のひとつに、Procをcase文の条件式に当てはめるというものがありました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def what_class(obj)
case obj
when proc { |x| x.kind_of? String }
String
when proc { |x| x.kind_of? Numeric }
Numeric
else
Class
end
end
what_class "string" #=> String
what_class 1 #=> Numeric
what_class [] #=> Class

メソッドの引数にブロックを受け取る

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# callで呼び出す場合
def convert_proc(&block)
block
end
proc_obj = convert_proc { puts 10 }
proc_obj.call #=> 10
# yieldを使う場合
def yield_proc
if block_given?
yield
else
'no block'
end
end
proc_obj2 = Proc.new { 'Proc block' }
p yield_proc &proc_obj2 #=> "Proc block"

Procとlambdaの違い

Procとlambdaの違いについて。まずはreturn, break, nextについて。

# return
Proc: Procの外のメソッドに影響する
lambda: lambda内でのみ影響する

# break
Proc: エラーが発生
lambda: returnと同じく処理が終了

# next
Proc, lambda: 処理を中断するのに使う

次に、引数の扱いについて。

1
2
3
p Proc.new { |x, y| x }.call(1) #=> 1
p lambda { |x, y| x }.call(1)
#=> `block in <main>': wrong number of arguments (1 for 2) (ArgumentError)

Procを使ったクロージャ

クロージャとは、引数以外にも関数定義のコンテキストに含まれる変数名のどの情報をもつ関数オブジェクトのことです。

1
2
3
4
5
6
7
8
9
10
def create_proc
str = 'from create_proc'
Proc.new { puts str }
end
proc_obj = create_proc
str = 'top level'
proc_obj.call #=> from crate_proc

Active Support::Concern

拡張モジュールを作成する際に有効活用できるのがActive Support::Concernです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'active_support/concern'
module IncludeModule
extend ActiveSupport::Concern
module ClassMethods
def hello_class_method
puts 'hello class method'
end
end
end
class IncludeClass
include IncludeModule
end
IncludeClass.hello_class_method #=> 'hello class method'

変更来歴

(02/05 22:00) 社畜から開放されてメタプロ勉強

(02/08 18:30) 奇跡の有給取得によりメタプロとか勉強@Coedo茅場町

(05-06 08:30) 言語仕様を追加

(05-10 17:30) メタプログラミング再勉強