酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

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

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

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

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


Included file ‘custom/google_ads_yoko_naga.html’ not found in _includes directory

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

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

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

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

メソッドの定義

Module#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』メソッドを使うと、継承関係を確認することができます。

Module#prepend

Ruby 2.0から追加されたメソッドで、Module#includeのようにクラスやモジュールに別のモジュールのメソッドを追加します。Module#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.0からはRefinementsによって同様のことができるようになりました。ただし、Ruby 2.0では実験的な機能として、ファイル単位に限定されていました。Ruby 2.1からはモジュール内でのみ特定のクラスのメソッドを書き換える事ができるようになりました。

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

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'

Module#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では定義済のクラスに対して、再度メソッドを定義し直したり、メソッドを追加できます。たとえば、0から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

ActiveSupport::Concern

拡張モジュールを作成する際に有効活用できるのがActiveSupport::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'

Included file ‘custom/google_ads_yoko_naga.html’ not found in _includes directory

変更来歴

(02/05 22:00) 社畜から開放されてメタプロ勉強
(02/08 18:30) 奇跡の有給取得によりメタプロとか勉強@Coedo茅場町
(05-06 08:30) 言語仕様を追加
(05-10 17:30) メタプログラミング再勉強

おすすめの書籍