SinatraはDSLなんかじゃない、Ruby偽装を使ったマインドコントロールだ!

ブログを下記に移転しました。デザイン変更により移転先では記事が一層読みやすくなっていますので、よろしければ移動をお願い致します。

SinatraはDSLなんかじゃない、Ruby偽装を使ったマインドコントロールだ! : melborne.github.com

                                                                                              • -


Sinatraのサイトを開くとSinatraDSLだと書いてある

Sinatra is a DSL for quickly creating web applications in Ruby with minimal effort:
(SinatraRubyで手早くWebアプリケーションをつくるためのDSLです)


DSLというのはDomain-Specific Language
つまり特定の目的に特化した言語のことだ
確かにSinatraはWebアプリケーションという
特定の目的のために作られたものだけれども
それは言語じゃない


それが言語といえるためにはオブジェクトのように
独立していて閉じてなきゃいけない*1
でもSinatraは独立も閉じてもなくて
Rubyに寄生することで存在している


いやSinatraは言語どころか
Rubyの上の専門用語ですらない


それが用語といえるためにはせりでの手やりのように
その専門の特別のルールで動かなきゃいけない*2
でもSinatraはそれ専用のルールで動いてなくて
Rubyのルールで動いてる

  # myapp.rb
  require 'sinatra'
  
  get '/' do
    'Hello world!'
  end


上のコードをRubyで実行すればSinatraが動くけど
コードの中のgetはRubyの関数*3呼び出しに過ぎない


irbで確かめてみよう

% irb -f
irb(main):001:0> get '/' do
irb(main):002:1*  'Hello world!'
irb(main):003:1> end
NoMethodError: undefined method `get' for main:Object
        from (irb):1
        from /Users/keyes/.rvm/rubies/ruby-1.9.2-p180/bin/irb:16:in `<main>'

Sinatraをrequireせずにgetを呼ぶと
Objectクラスのインスタンスmainにはgetメソッドは未定義とのエラーが出た


今度はSinatraをrequireしてgetを呼んでみよう

irb(main):004:0> require 'sinatra'
=> true
irb(main):005:0> get '/' do
irb(main):006:1*  'Hello world!'
irb(main):007:1> end
=> [/^\/$/, [], [], #<Proc:0x00000101331218@/Users/keyes/.rvm/gems/ruby-1.9.2-p180/gems/sinatra-1.2.6/lib/sinatra/base.rb:1152>]

今度はgetが呼べた
そしてRegexpオブジェクトやProcオブジェクトを含んだ
Arrayオブジェクトが返された


それじゃカッコを省略せずにRubyの礼儀正しい構文で呼んでみよう

irb(main):008:0> get('/') { 'Hello world!' }
=> [/^\/$/, [], [], #<Proc:0x0000010133bce0@/Users/keyes/.rvm/gems/ruby-1.9.2-p180/gems/sinatra-1.2.6/lib/sinatra/base.rb:1152>]
irb(main):009:0> 

同じ結果が返ってきた


ここでRubyのトップレベルで呼べる関数は
Objectクラスに定義された
プライベート・インスタンスメソッドであった
このことをputsで確認してみよう

irb(main):024:0> Object.private_instance_methods.grep /^puts/
=> [:puts]


しかしその定義の実態はObjectクラスにはなくて
それにインクルードされたKernelモジュールにあるのだった

irb(main):025:0> Object.private_instance_methods(false).grep /^puts/
=> []
irb(main):026:0> Kernel.private_instance_methods(false).grep /^puts/
=> [:puts]


じゃあ同様にgetもsinatraをrequireしたことによって
Objectクラスに定義されているはずだ

irb(main):027:0> Object.private_instance_methods.grep /^get/
=> [:get, :gets]

あった


さてその実態はやはりKernelにあるのだろうか

irb(main):028:0> Object.private_instance_methods(false).grep /^get/
=> []
irb(main):031:0> Kernel.private_instance_methods(false).grep /^get/
=> [:gets]

Kernelにはなかった...


そうすると想像できるのは
sinatraのrequireによってObjectクラスに
別のモジュールがインクルードされたということだ
確かめてみよう

irb(main):032:0> Object.included_modules
=> [Sinatra::Delegator, Kernel]


Sinatra::Delegatorというモジュールがインクルードされていた
じゃあここにgetメソッドが定義されているんだろう

irb(main):061:0> Sinatra::Delegator.private_instance_methods(false).grep /^get/
=> [:get]

やはりそうだった
Sinatraソースコードで中身を確認してみよう

# base.rb
module Sinatra
  module Delegator #:nodoc:
    def self.delegate(*methods)
      methods.each do |method_name|
        eval <<-RUBY, binding, '(__DELEGATE__)', 1
          def #{method_name}(*args, &b)
            ::Sinatra::Application.send(#{method_name.inspect}, *args, &b)
          end
          private #{method_name.inspect}
        RUBY
      end
    end

    delegate :get, :put, :post, :delete, :head, :template, :layout,
             :before, :after, :error, :not_found, :configure, :set, :mime_type,
             :enable, :disable, :use, :development?, :test?, :production?,
             :helpers, :settings
  end
end


ちょっと分かりづらいけど
要はDelegator.delegateでDelegatorモジュールに
getプライベート・インスタンスメソッドを生成している
そしてその中身は受け取った引数とブロックをそのまま
Sinatra::Applicationクラスに定義されたgetクラスメソッドに
移譲するものとなっている


つまりsinatraをrequireしてトップレベルでgetを呼ぶと
Delegatorモジュールを介してSinatra::Applicationクラスの
getクラスメソッドが呼ばれる


irbで直接これを呼んで確かめてみよう

irb(main):037:0> Sinatra::Application.get('/') { "hello, world" }
=> [/^\/$/, [], [], #<Proc:0x0000010131a9f0@/Users/keyes/.rvm/gems/ruby-1.9.2-p180/gems/sinatra-1.2.6/lib/sinatra/base.rb:1152>]

期待通りの結果が返ってきた
じゃあその定義があるか確認してみよう

irb(main):051:0> Sinatra::Application.singleton_methods(false).grep /^get/
=> []

無い...
Sinatra::Applicationにはスーパークラスがあるのかな?

irb(main):053:0> Sinatra::Application.superclass
=> Sinatra::Base

Sinatra::BaseというのがSinatra::Applicationのスーパークラスだった

irb(main):055:0> Sinatra::Base.singleton_methods(false).grep /^get/
=> [:get]

getの定義はここにあった


一応ソースを確認してみよう

module Sinatra
  class Base
    class << self
      def get(path, opts={}, &block)
        conditions = @conditions.dup
        route('GET', path, opts, &block)

        @conditions = conditions
        route('HEAD', path, opts, &block)
      end

      def put(path, opts={}, &bk);    route 'PUT',    path, opts, &bk end
      def post(path, opts={}, &bk);   route 'POST',   path, opts, &bk end
      def delete(path, opts={}, &bk); route 'DELETE', path, opts, &bk end
      def head(path, opts={}, &bk);   route 'HEAD',   path, opts, &bk end

    private
      def route(verb, path, options={}, &block)
        # Because of self.options.host
        host_name(options.delete(:bind)) if options.key?(:host)

        options.each {|option, args| send(option, *args)}

        pattern, keys = compile(path)
        conditions, @conditions = @conditions, []

        define_method "#{verb} #{path}", &block
        unbound_method = instance_method("#{verb} #{path}")
        block =
          if block.arity != 0
            proc { unbound_method.bind(self).call(*@block_params) }
          else
            proc { unbound_method.bind(self).call }
          end

        invoke_hook(:route_added, verb, path, block)

        (@routes[verb] ||= []).
          push([pattern, keys, conditions, block]).last
      end
    end
  end
end


要するにこういうことだ
sinatraをrequireするとトップレベルに書かれたgetは
あたかもSinatra::Baseクラスの中に書かれたように解釈されて
そこに定義されたクラスメソッドが呼ばれるのだ


試しにSinatra::Base.getを再定義してその効果を見てみよう

irb(main):075:0> class Sinatra::Base
irb(main):076:1>   def self.get(path)
irb(main):077:2>     {path.intern => yield}
irb(main):078:2>   end
irb(main):079:1> end
=> nil
irb(main):080:0> get '/' do
irb(main):081:1*  "hello , world!"
irb(main):082:1> end
=> {:/=>"hello , world!"}

うまくいった


つまり
SinatraはほんとうはRubyそのものなんだけど
その構文のユルさとメタプログラミングを使って専用言語を装い
ユーザをその独自の世界に引き込むべく
僕らをマインドコントロールしてたんだ!


もう僕はダマされないぞ!


関連記事:

  1. Sinatraに別構文があってもいいじゃないか!
  2. 内部DSLを使って、RubyのWebフレームワークを書こう!