reona.dev

メタプログラミングRuby 1-3章


今回はしばらく積読となっていたメタプログラミング Ruby を読んだので、内容についてまとめていきます。

想定読者

想定読者については次のように書かれていました。

  • 最初の章のコードを何の問題もなく理解できるひと # いくつもサンプルコードがでてくるので割愛します。
  • 配列を繰り返すのにどのようなコードを書くか?
    • each メソッドを思い浮かべたなら文章を読み進めるだけの Ruby の知識がある。
    • for 文を思い浮かべたなら Ruby 初心者 → 初心者でも Ruby 入門書を読みつつメタプロに入門できる
  • Ruby や Ruby on Rails の内部の動きを深く理解したい人

個人的には Ruby の開発プロジェクト経験がある方なら読めると思いました。

本書でできること

本のタイトル通りですが、Ruby を使ったメタプログラミング ができるようになります。 メタプログラミングとはなんぞや?という話ですが、それも本書で詳しく解説されています。 また、メタプログラミングをするために Ruby や Ruby on Rails の内部の動きを探っていくことで言語仕様の理解が深まります。

1 章 頭文字 M

冒頭で「メタプログラミングとは、コードを記述するコードを記述することである。」と書かれています。 また途中には「メタプログラミングとは、言語要素を実行時に操作するコードを記述することである。」と書かれています。

この定義の例として、Active Record が挙げられています。 例えば Ruby で Movie クラスがあるとして、Movie クラスのオブジェクト内部の変数を読み書きしたい場合はアクセサメソッドを用意する必要があります。 しかし、ActiveRecord::Base を継承することで、コード実行時にアクセサメソッドを定義してくれます。 これがさきほど挙げた、言語要素を実行時に操作するコードを記述するを表していますね。

Ruby はスクリプト言語であり、コンパイルをしなくてもいいので実行時に言語要素の殆どにアクセスが可能です。だから、コード実行時にメソッドを定義したり、もともとの定義を書き換えたりできます。メタプログラミングと非常に相性の良い言語が Ruby であり、メタプログラミングを習得することで Ruby の力をより引き出すことができます。

2 章 月曜日:オブジェクトモデル

この章からは主人公のボブとメンターのビルの対話形式で進行していきます。

  • class キーワードで既存のクラスを再オープンして、その場で変更を加えることができる。 => オープンクラス
  • オープンクラスの性質を利用して既存クラスにメソッドを追加したり、すでにクラスに定義されているメソッドを再定義する。 => モンキーパッチ
    • モンキーパッチをすると、以後そのクラスを使う際にも定義が変わってしまうので非常に影響が大きい。Refinements を使うと、ファイルやモジュール定義が終わるところまでにパッチを当てるスコープを制限できる。
module PrintExtension
  refine Kernel do
    def puts
      'puts!!!'
    end
  end
end
 
class User
  puts # => nil
 
  using PrintExtension
 
  puts # =>
  - puts!!!
end
 
puts # => nil

上は極端な例だが、Refinements を使えばメソッドのオーバーライドによる予期しないバグを防ぐことができるかもしれないですね。

  • インスタンスメソッドは #method 、クラスメソッドは .method で表す。
    • 例えばクラスやオブジェクトが method を持っているか調べる際は、 obj.methods.grep(/me/) を使う。
  • クラスは BasicObject まで続く継承チェーンがある。
class User
  include Animal
  include Person
end
 
user = User.new
user.print
pry(main)> User.ancestors
=> [User,
 Person,
 Animal,
 Object,
 PP::ObjectMixin,
 Kernel,
 BasicObject]
 
// PP::ObjectMixinはpryが勝手に用意した整形用の定数でirbでは表示されない

module を class に include することで、継承チェーンで言うと class の真上に module が挿入され、それより上のチェーンは押し上げます。 include でなく prepend を使えば、class の下に module が挿入されます。

Object クラスは Kernel モジュールを継承しているので、Kernel にメソッドを追加することでカーネルメソッドがすべてのオブジェクトで使えるようになります。

オブジェクトのメソッドはクラス(モジュール)に住んでおり、継承チェーンのなかで複数のクラスが同名のメソッドを持っていた場合は、先に呼ばれたクラスのメソッドが使わます。 user の print がどのクラスのメソッドなのか探索する際には、User クラスから上に向かって調べていくとよいでしょう。

pry(main)> Kernel.methods.grep(/print/)
=> [:sprintf,
 :printf,
 :print,
 :pretty_print,
 :pretty_print_cycle,
 :pretty_print_instance_variables,
 :pretty_print_inspect]
  • メソッドを定義する際は、メソッドのレシーバが self となる。
    • メソッド定義の外(クラスやモジュールの中)での self は、そのクラス自身が self になる。
    • self は省略可能。省略した場合も、レシーバが self のメソッドだとされる。
    • 自分以外のオブジェクトを呼び出す際には明示的にレシーバにそのオブジェクトを指定する必要がある。一方で、private メソッドは明示的にレシーバを指定して呼び出すことはできない。そのため、private メソッドは自分以外は呼び出すことができない。
      • Object#send を使うことで、private メソッドにアクセス可能

3 章 火曜日:メソッド

  • 動的メソッド Object#send を呼び出すことで、メソッドを引数に取ることができる。
  • Module#define_method を使えば、メソッドをその場で定義できる。(実行時にメソッド名を決定できる)
  • BasicObject クラスにはメソッドが見つからなかった際に最終的に呼び出される method_missing が用意されている。 method_missing を再定義することで、存在しないメソッドを呼び出すことができる。 => ゴーストメソッド

「可能であれば動的メソッドを使い、仕方がなければゴーストメソッドを使う」

と締めくくられていましたが、 #send もクラスやモデル、テーブルなどの設計次第では使う場面はあまりない気がしました。(それでもメソッドを引数にとれるのは便利。) メタプロ Ruby のサンプルコードはテーブル設計がひどく、そのようなプロジェクトにあたった際の対症療法としてはいいかもしれません。 #send を使いたいと思った時点で設計を見直してもいいかもしれないですね。 とはいえ Ruby は非常に柔軟で強力な言語だと再認識しました。

メタプロはとりあえず「難しそう」という先入観が強く後回しにしていましたが、読み始めるとひたすらに Ruby という言語を掘り下げていく内容で、夢中になって読み進めることができています。これ以降の章も別の記事でまとめたいと思います。