SinatraはDSLなんかじゃない、Ruby偽装を使ったマインドコントロールだ!
Sinatraのサイトを開くとSinatraはDSLだと書いてある。
Sinatra is a DSL for quickly creating web applications in Ruby with minimal effort: (SinatraはRubyで手早く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そのものなんだけど、その構文のユルさとメタプログラミングを使って専用言語を装い、ユーザをその独自の世界に引き込むべく僕らをマインドコントロールしてたんだ!
もう僕はダマされないぞ!
関連記事:
(comment)
> Try http://github.com/padrino/padrino-framework
»Dexterさん
情報どうもです
blog comments powered by Disqus