はじめに
これは私がRuby Goldを受験に向けて勉強する中で、主にメタプログラミングで役立ったこと・知らなかったことをまとめた記事になります。
何気なく使っていたことでも、勉強するうちに、じゃあこれは何でそうなるのか?これはどうやったら動くのか?と思うことが増えたのですが、学び理解することで以前に比べ明確な意思を持ってコーディングできるようになったと思います。
メタプロと言っても、その範囲は膨大なため、特に普段から使える、使っていたけどなんで動いてるのかわかってなかったものを5つ 6つピックアップしてみました。(絞り込みたかったのに絞り込めませんでした)
メタプロやったことない!という方や、メタプロ知ってるけど復習したい!という方の役に立てば幸いです。
環境
MacOS(irbが使える環境であれば問題ありません)
Ruby 2系(筆者の実行環境は 2.6.3
)
メタプログラミングとは?
メタプログラミングとは、言語要素を実行時に操作するコードを記述することである。
もっと端的に言うなら、 コードを記述するためのコードを記述する
のがメタプログラミングです。
例えばActive Record
はActiveRecord::Base
クラスを継承するだけで、実行時に各要素へのアクセサメソッドが定義されますよね。ActiveRecord::Base
にはそういった定義がされていて、その定義の仕方こそがコードを記述するためのコードを記述すること
なのです。
(※Rails
でmodel
を作る時、対象のModelはApplicationRecord
を継承させると思いますが、そのApplicationRecord
はActiveRecord::Base
を継承しています。)
これをみるとなんとなく、メタプロって自分で使うことは滅多にないんじゃない?
と思う方が出てくるかもしれません。少なくとも、学習当初は私自身がそう思っていました。
けれど、実際に学習すると、無意識のうちに自分がメタプログラミングを使用していたことに気付きます。Ruby
という言語がメタプロと密接に関係しているからです。
今回は「メタプロっぽくないけど実はメタプロ」という項目について触れていきます。
クラスについて
定数の相対参照時の探索順
定数はRuby
の厄介な要素の1つだと思っています。
何故なら、定数をなんとなくで使うと「一見取得できそうな値が取れない」という事象が多発するからです。
以下のソースを実行した時、コンソールには Taro
と Ichiro
どちらが表示されると思いますか。
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_a
と new_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_a
を undef
(メソッド定義を消す) しても、エイリアスを付けた方のメソッドは生き残ります。すごい。
じゃあaliasって何してんの?という話なんですけど、こちらに書いてありました。
別名を付けられたメソッドは、その時点でのメソッド定義を引き継ぎ、元のメソッドが再定義されても、再定義前の古いメソッドと同じ働きをします。あるメソッドの動作を変え、再定義するメソッド で元のメソッドの結果を利用したいときなどに利用されます。
ほーん
ブロックについて
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_a
とsample_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 オブジェクトを返すことが期待されます。
シンボルは 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を作る記事を書きたかったのですが、次回に持ち越します……。