Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

メタプログラミングの隠れたコストとは?知っておくべき3つのデメリット

「メタプログラミング」という言葉は、とても高度で洗練された技術のように聞こえますが、果たして本当に良いものなのでしょうか?

結論から言えば、使いどころ次第で非常に有用です。しかし多くの開発者は、メタプログラミングの利用には一定のコストが伴うことを理解していません。

まずは認識を合わせるために…

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

ここでは、メタプログラミングを以下のような手法全般と定義します。

  • コードの構造そのものを変更する(例:define_method
  • 文字列を、あたかも実際のRubyコードの一部であるかのように実行する(例:instance_eval
  • 何らかのイベントへの反応として動作する(例:method_missing

では、メタプログラミングには具体的にどのようなコストがあるのでしょうか。筆者はこれらを3つのグループに分類しています。

  • 速度
  • 可読性
  • 検索性

補足: 4つ目のグループとしてセキュリティを挙げることもできます。これはeval系メソッドが原因です。evalは渡された内容に対していかなるセキュリティチェックも行わないため、チェックは開発者自身が責任を持って行う必要があります。

それでは、それぞれのコストを詳しく見ていきましょう!

1. 速度

最初のコストは速度です。ほとんどのメタプログラミング用メソッドは、通常のメソッドよりも遅くなります。

以下はベンチマーク用のコードです。

require 'benchmark/ips'

class Thing
  def method_missing(name, *args)
  end

  def normal_method
  end

  define_method(:speak) {}
end

t = Thing.new

Benchmark.ips do |x|
  x.report("normal method")  { t.normal_method }
  x.report("missing method") { t.abc }
  x.report("defined method") { t.speak }

  x.compare!
end

実行結果(Ruby 2.2.4):

normal method:   7344529.4 i/s
defined method:  5766584.9 i/s - 1.34x  slower
missing method:  4777911.7 i/s - 1.54x  slower

ご覧のとおり、メタプログラミング系のメソッド(define_methodmethod_missing)は、通常のメソッドよりかなり遅いことがわかります。

ここで興味深い発見がありました…

上記の結果はRuby 2.2.4によるものですが、Ruby 2.3Ruby 2.4で同じベンチマークを実行すると、これらのメソッドはさらに遅く見えるのです!

Ruby 2.4でのベンチマーク結果:

normal method:   8252851.6 i/s
defined method:  6153202.9 i/s - 1.39x  slower
missing method:  4557376.3 i/s - 1.87x  slower

偶然の結果ではないことを確認するため、このベンチマークは何度も繰り返し実行しました。

しかし、1秒あたりの実行回数(i/s)に注目すると、実は通常のメソッド自体がRuby 2.3以降で高速化されていることがわかります。それが相対的にmethod_missingを大きく遅く見せていたのです 🙂

2. 可読性

instance_eval / class_evalメソッドを使うと、エラーメッセージがあまり役に立たなくなることがあります。

次のコードを見てください。

class Thing
  class_eval("def self.foo; raise 'something went wrong'; end")
end

Thing.foo

これを実行すると、次のようなエラーが出力されます。

(eval):1:in 'foo': 'something went wrong...' (RuntimeError)

ファイル名が表示されず(代わりにevalと表示される)、正しい行番号も失われていることに注目してください。幸い、この問題には解決策があります。eval系メソッドは、追加の引数を2つ受け取ることができるのです。

  • ファイル名
  • 行番号

組み込み定数の__FILE____LINE__class_evalの引数として渡せば、エラーメッセージに正しい情報が表示されるようになります。

:

class Thing
  class_eval(
    "def foo; raise 'something went right'; end",
    __FILE__,
    __LINE__
  )
end

なぜこれがデフォルトの動作になっていないのかは謎ですが、これらのメソッドを使う際には覚えておきたいポイントです 🙂

3. 検索性

メタプログラミングを使うと、コードの検索性が下がり、ドキュメントの質も低下し、デバッグも難しくなります。

メソッド定義を探すとき、通常ならCTRL+F(またはお好みのショートカット)で簡単に見つけられますが、メタプログラミングで定義されたメソッドはそうはいきません。特に、メソッド名が実行時に動的に組み立てられる場合は顕著です。

次の例では、メタプログラミングによって3つのメソッドを定義しています。

class RubyBlog
  def create_post_tags
    types = ['computer_science', 'tools', 'advanced_ruby']

    types.each do |type|
      define_singleton_method(type + "_tag") { puts "This post is about #{type}" }
    end
  end
end

rb = RubyBlog.new

rb.create_post_tags
rb.computer_science_tag

YardRDocのようなドキュメント生成ツールは、これらのメソッドを見つけて一覧にすることができません。

これらのツールは「静的解析」と呼ばれる手法でクラスやメソッドを検出しています。この手法で見つけられるのは、def構文で直接定義されたメソッドだけなのです。

先ほどの例でyard docを実行してみると、検出されるのはcreate_post_tagsだけであることが確認できます。

実際の出力はこのようになります:

メタプログラミングの隠れたコストとは?知っておくべき3つのデメリット

@methodタグを使えばyardに追加のメソッドをドキュメント化させることも可能ですが、必ずしも実用的とは言えません。

:

class Thing
  # @method build_report
  define_method(:build_report)
end

また、grepack、エディタの検索機能などでメソッド定義を探す場合でも、メタプログラミングによるメソッドは通常のメソッドよりも見つけにくくなります

「Sidekiqでは一切メタプログラミングを使っていない。95%の場合、コードを読みにくくするだけでメリットがないと思うからだ」 – Mike Perham(Sidekiq作者)

まとめ

メタプログラミングのすべてが悪いわけではありません。適切な場面で使えば、コードをより柔軟にする強力な武器になります。

大切なのは、こうした隠れたコストを把握したうえで、より良い設計判断を下せるようになることです。

この記事が役に立ったら、ぜひシェアしてください 🙂

  1. 無制限で使える無料VPNおすすめ3選|知っておきたい隠れたコストとは

    VPNを利用するメリットはよく知られています。日々の作業環境にVPNを取り入れようと考えたとき、まずは無料のサービスから試してみたいと思う方も多いでしょう。しかし、多くの無料VPNにはデータ通信量の上限が設けられているため、「無制限で使える無料VPN」を探すことになります。 本記事では、帯域幅に制限のかからないおすすめの無料VPNを3つご紹介します。無料VPNには大きなリスクが伴うことも少なくないため、それぞれのサービスに潜む「隠れたコスト」についても詳しく解説していきます。 注意: 以下の速度テストの基準として、執筆時点でVPN未使用の状態でSpeedtest.netを計測した結果は、Pin

  2. Rubyのエイリアス(別名定義)完全ガイド:aliasキーワードとalias_methodの違い

    Rubyでは、既存のメソッドに別名(エイリアス)を付ける方法が2つあります。 alias(キーワード) alias_method(メソッド) どちらも同じ目的で使えますが、挙動が微妙に異なるため、初心者にとって混乱しやすいトピックです。 本記事では、両者の違いを詳しく掘り下げ、しっかりと理解できるように解説していきます。 aliasキーワードとは まずはaliasから見ていきましょう。aliasはRubyのキーワードの一つです(ifやdef、classなどと同じ扱いです)。 基本的な書き方は以下の通りです。 alias print_something puts print_someth