32
17

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 3 years have passed since last update.

Ruby メタプログラミング 入門〜わかってたつもりだけどわかってなかった6選〜

Last updated at Posted at 2020-01-28

はじめに

これは私がRuby Goldを受験に向けて勉強する中で、主にメタプログラミングで役立ったこと・知らなかったことをまとめた記事になります。
何気なく使っていたことでも、勉強するうちに、じゃあこれは何でそうなるのか?これはどうやったら動くのか?と思うことが増えたのですが、学び理解することで以前に比べ明確な意思を持ってコーディングできるようになったと思います。
メタプロと言っても、その範囲は膨大なため、特に普段から使える、使っていたけどなんで動いてるのかわかってなかったものを5つ 6つピックアップしてみました。(絞り込みたかったのに絞り込めませんでした:innocent:
メタプロやったことない!という方や、メタプロ知ってるけど復習したい!という方の役に立てば幸いです。

環境

MacOS(irbが使える環境であれば問題ありません)
Ruby 2系(筆者の実行環境は 2.6.3

メタプログラミングとは?

メタプログラミングとは、言語要素を実行時に操作するコードを記述することである。

もっと端的に言うなら、 コードを記述するためのコードを記述するのがメタプログラミングです。
例えばActive RecordActiveRecord::Baseクラスを継承するだけで、実行時に各要素へのアクセサメソッドが定義されますよね。ActiveRecord::Baseにはそういった定義がされていて、その定義の仕方こそがコードを記述するためのコードを記述することなのです。

(※Railsmodelを作る時、対象のModelはApplicationRecordを継承させると思いますが、そのApplicationRecordActiveRecord::Baseを継承しています。)

これをみるとなんとなく、メタプロって自分で使うことは滅多にないんじゃない?と思う方が出てくるかもしれません。少なくとも、学習当初は私自身がそう思っていました。
けれど、実際に学習すると、無意識のうちに自分がメタプログラミングを使用していたことに気付きます。Ruby という言語がメタプロと密接に関係しているからです。
今回は「メタプロっぽくないけど実はメタプロ」という項目について触れていきます。

クラスについて

定数の相対参照時の探索順

定数はRubyの厄介な要素の1つだと思っています。
何故なら、定数をなんとなくで使うと「一見取得できそうな値が取れない」という事象が多発するからです。
以下のソースを実行した時、コンソールには TaroIchiro どちらが表示されると思いますか。


class Parent
  NAME = "Ichiro"

  def name
    NAME
  end
end

class Yamada < Parent
  NAME = "Taro"
end

puts Yamada.new.name








答えは Ichiro です。私は最初 Taro が表示されるのだと思っていました。
定数はインスタンスに存在するのではなく、クラスに存在します。
#name 内の NAME のような定数の書き方を相対参照と呼びます。
相対参照で参照した場合、探索対象の定数は、selfではなく定数を呼び出した、メソッド定義位置から探索が始まります。その為、#nameが定義してあるHumanクラスNAME = Ichiroが呼び出されたというわけです。

以下の場合はどうでしょう。

module Human
  module Infant
    GREET = 'Hello!'

    class Parent
      GREET = 'Hello, mum!'
    end

    class Yamada < Parent
      def greet
        puts GREET
      end     
    end
  end
end

Human::Infant::Yamada.new.greet

Yamadaくんはなんと挨拶すると思いますか。
実行して確認してみてください。






これは定数の相対参照時の探索順の問題になります。
定数の相対参照の場合、まずレキシカルスコープ、次に継承関係にあるクラス、といった順番で定数を探しにいきます。
レキシカルスコープとはソースコード上の物理的な位置のことです。難しいですね。
私的に分かりやすく言い換えると、 自分自身の持っている情報 を確認した後に 祖先を確認しにいく、ということです。
ここでいう自分自身とは Human::Infant::Yamada です。 Human, Infant, Yamada の中で該当する定数がないかをまず探します。
もし該当するものがレキシカルスコープ内にないのであれば、今度は継承関係のある祖先の中に定数を持っているものがいないかを探しにいきます。
以上の探索順から、 Infant に定義されている GREET の値が参照されるわけです。

定数には絶対参照や修飾つき参照もあるので、自信がない方は確認すると良いと思います。
ややこしいところはあれど、定数について理解してからは、定数で悩む機会が少なくなりました。

参考:Rubyの定数が怖いなんて言わせない (例題もあり、面白い記事でした。分かりやすかったです。例題が解けた時の気持ちよさは格別でした。)

alias

alias。メソッドに別名をつける為に何気なく使っていましたが、エイリアスを付けた後の振る舞いについて深く考えたことがなかったのでその動きを確認した時驚きました。
以下のように書くことでメソッドに別名をつけることができます。

class A
  def method_a
    puts 'A'
  end

  alias new_a method_a
end

A.new.new_a

では以下のように method_a を再定義した場合、 method_anew_a の返り値はどうなるでしょうか。

class A
  def method_a
    puts 'A'
  end

  alias new_a method_a

# 上記 method_a と同名の新しいメソッドを再定義
  def method_a
    puts 'B'
  end
end

A.new.method_a
A.new.new_a

new_a については alias を定義した時点の情報が保持されていることがわかります。
method_aundef (メソッド定義を消す) しても、エイリアスを付けた方のメソッドは生き残ります。すごい。

じゃあaliasって何してんの?という話なんですけど、こちらに書いてありました。

別名を付けられたメソッドは、その時点でのメソッド定義を引き継ぎ、元のメソッドが再定義されても、再定義前の古いメソッドと同じ働きをします。あるメソッドの動作を変え、再定義するメソッド で元のメソッドの結果を利用したいときなどに利用されます。

ほーん:open_mouth:

ブロックについて

yield&修飾って何となく使っていませんか?私はそうでした。仕組みを理解した上で使うと「何でそう動くか」ということが明確になります。

yield

メソッドにブロックを渡すと、yieldキーワードでブロックをコールバックできるのはみなさんご存知だと思います。
yieldは引数を受け取ることができます。例えば、以下のようなメソッドがあったとします。


def sample_method(x, y)
 x + yield(x, y)
end

以下のように実行するとどうなるでしょうか?

sample_method(2, 4) { |a, b| (a + b) * 3 }





2 + ((2 + 4) * 3)の結果として20と表示されると思います。
ちなみにyieldで受け取る引数の数と、ブロック変数の数は異なっても問題がありません。

ブロックの存在は Kernel#block_given?メソッドを使用して確認することができます。
ブロックが存在しない場合にyieldを使用するとエラーになります。実装時には気をつけなければなりません。

ところでブロックって結局何だ?

と、思ったことはありませんか。何気なく使っていたけれど、その実態を私は理解していませんでした。

ブロックとは、メソッド呼び出しの際に引数と一緒に渡すことのできる処理のかたまりのことです。

たのしいRuby には記載されています。
ブロックはオブジェクトではありません。ではブロックをオブジェクトとして扱いたい時はどうすればいいのでしょうか。

Proc(lambda)

メソッドでブロックを受け取って yield で実行する以外にも、Procオブジェクトとしてブロックを持ち運び、任意の場所で実行することができます。
以下のソースを実行してみてください。


say_word = Proc.new do |word|
  puts "say #{word}"
end

say_word.call('hello')
say_word.call('good-bye')

Procオブジェクトは、他のオブジェクトと同様にインスタンスを生成して使用します。インスタンス生成時にブロック内に記載した処理を、 Proc#call を使用した際に実行します。callメソッドの引数に渡した値はブロック引数として使用します。
※ちなみに以下のような書き方でも実行できます。

  • say_word['hello']
  • say_word.yield('hello')
  • say_word.('hello')
  • say_word === 'hello' (※引数がある時のみ)

&修飾

では引数で受け取ったブロックを別のメソッドに渡したい時、どうしたらいいでしょうか。
ブロックを渡されたメソッド内で別メソッドを読んだ場合、別メソッドにもブロック渡されるんじゃない?と私は思っていました。
以下のコードを実行してみてください。実際にどうなるかがわかります。


def sample_a
  if block_given?
    puts 'Yes:a'
  else
    puts 'No:a'
  end
  sample_b
end

def sample_b
  if block_given?
    puts 'Yes:b'
    yield
  else
    puts 'No:b'
  end
end

sample_a { puts 'I am a block' }

ブロックは渡されたメソッド内でのみ有効だということが分かります。
では渡されたブロックを別メソッドに渡したい時はどうしたらいいのでしょうか。
ここで役立つのが先ほど出てきた Procオブジェクトです。
&修飾を使うことで受け取ったブロックを Proc オブジェクトとして使用することができます。(Procインスタンスが生成される=>メタ!)
以下の書き出しを元に、期待する実行結果が得られるようにsample_asample_bを書き換えてみてください、

def sample_a(&block)

# 期待値(以下のように出力される)
Yes:a
Yes:b
I am a block






答えは以下です。


def sample_a(&block)
  if block_given?
    puts 'Yes:a'
  else
    puts 'No:a'
  end
  sample_b(&block)
end

def sample_b(&block)
  if block_given?
    puts 'Yes:b'
    block.call
  else
    puts 'No:b'
  end
end

sample_a { puts 'I am a block' }

こうすることでブロックをProcオブジェクトとして別メソッドに渡すことができるようになりました。
blockはProcオブジェクトなので、実行するにはcallメソッドを呼ぶ必要があります。

この &修飾 、どこかで見覚えがありませんか。

%w(a b c).map(&:upcase)

=> ["A", "B", "C"]

これです。
この形、結構よく使いますが、実際にどんな風に動いているかご存知でしょうか。
&修飾 を使用していることからProcが関わっているということは察しがつくと思います。


p = Proc.new {|word| word.upcase }
%w(a b c).map(&p)

こう記述すると今までの知識を元に行われていることをイメージすることができます。
では&修飾にシンボルを渡した時はどうなるのでしょうか。

to_proc メソッドを持つオブジェクトならば、`&' 修飾した引数として渡すことができます。デフォルトで Proc、Method オブジェクトは共に to_proc メソッドを持ちます。to_proc はメソッド呼び出し時に実行され、Proc オブジェクトを返すことが期待されます。

メソッド呼び出し(super・ブロック付き・yield)

シンボルは to_proc メソッドを持っています。

self に対応する Proc オブジェクトを返します。
生成される Proc オブジェクトを呼びだす(Proc#call)と、 Proc#callの第一引数をレシーバとして、 self という名前のメソッドを 残りの引数を渡して呼びだします。

つまり、(&:upcase)は、実行されると動的にProcオブジェクトを生成し、それを実行していると言うことになります。つまりこれもまた黒魔術なのです。私は自覚なく黒魔術を1年使っていたということに気付いた時、とても驚きました。「Procなんていつ使うの?」なんて学習当初に思っていたのが恥ずかしいくらいです。凄く使っていました。

※ちなみに &mehtod も同じ理屈です。Procってすごい。

小ネタ

メタプロらしいメタプロがあんまりなかった気がしたので、私の中のメタプロといえばこれ、みたいなイメージをピックアップしました。

Module#define_method(メソッドの動的定義)

class DefineMethodSample
  %w(a b c).each do |str|
    define_method str do
      puts str
    end
  end
end

# そのクラス内で定義されているインスタンスメソッドが配列で取得できる
DefineMethodSample.public_instance_methods(false)

DefineMethodSample.new.a
DefineMethodSample.new.b
DefineMethodSample.new.c

Object#send(メソッドの動的呼び出し)

class SendSample
  def show_word(word)
    puts "It is a #{word}"
  end
end

# Object#sendは動的にメソッドを呼び出せる。第2引数にメソッドに渡す引数を指定できる
SendSample.new.send(:show_word, 'cake')

最後に

何気なく使っていたことも、紐解いてみると黒魔術に塗れていたことがわかりました。
継承とかevalとか、実は苦しんだprivateメソッドについて省略してしまったので、別記事でまとめたいです。
まだまだ理解しきれていない部分も多いですが、メタプロってパズルみたいで面白いですね。
本当はなんちゃってRSpecを作る記事を書きたかったのですが、次回に持ち越します……。

参考

メタプログラミング Ruby 第2版
たのしいRuby 第6版

32
17
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
32
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?