4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Sinatraを支えるRuby記法 - 登録編

Last updated at Posted at 2019-05-22

はじめに

SinatraはWebアプリケーションを記述するためのDSL(を実装するライブラリ)です。
READMEでは以下のようにアプリケーションを記述し、

myapp.rb
require 'sinatra'

get '/' do
  'Hello world!'
end

このrbファイルを実行すればすぐにサーバを立ち上げることができると書かれています(もちろんこれだけじゃ実用的ではないというところは置いといて)

ruby myapp.rb

この記事の目的はSinatraの使い方を説明することではなく、上のように書いたDSLが「どうやって動いているのか(動かされているのか)」を調べることで、明日役に立つ(ことがあるかもしれない)知識を学ぶことにあります。

Sinatraを読んでいくにあたって以下の三段階に分けて説明していきます。

なお、読解対象とする(記事でリンクを張る)Sinatraのバージョンは2.0.5とします。

sinatra/main.rbとextend

アプリケーションのファイルがrequireしているsinatra.rbは、sinatra/main.rbをrequireしています。さらにsinatra/main.rbはsinatra/base.rbをrequireしており、このsinatra/base.rbが一番核となるファイルなのですがまずはsinatra/main.rbを見てみましょう。以下の記述があります(上の方に書いてある部分は起動編で説明します)

sinatra/main.rbより抜粋
# include would include the module in Object
# extend only extends the `main` object
extend Sinatra::Delegator

Sinatra::Delegatorがextendされています。extendは「モジュールに定義されたメソッドをオブジェクトの特異メソッドとして追加する(オブジェクトのクラス全部にではなく特定のオブジェクトのみにメソッドを追加する)」という動作をします。
上記ではextendはオブジェクトを指定することなく書かれています。実はRubyのトップレベル(class内とかじゃない単にputsとか書くところ)はmainというオブジェクトがself(メソッドのレシーバー)になっています。

つまり、「extend Sinatra::Delegator」により、Delegatorに定義されているメソッドがトップレベルにメソッドとして定義されます。先に書いておくとこのDelegatorにgetなどのルーティングメソッドが定義されています。
Delegator(移譲者)という名前から想像できるようにDelegatorには単純にメソッドが定義されているわけではありません。というわけで次にsinatra/base.rbに入りSinatra::Delegatorを見てみましょう。

Sinatra::Delegator

Sinatra::Delegatorの定義はsinatra/base.rbの最後の方にあります。

sinatra/base.rbより抜粋
  # Sinatra delegation mixin. Mixing this module into an object causes all
  # methods to be delegated to the Sinatra::Application class. Used primarily
  # at the top-level.
  module Delegator #:nodoc:
    def self.delegate(*methods)
      methods.each do |method_name|
        define_method(method_name) do |*args, &block|
          return super(*args, &block) if respond_to? method_name
          Delegator.target.send(method_name, *args, &block)
        end
        private method_name
      end
    end

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

    class << self
      attr_accessor :target
    end

    self.target = Application
  end

Delegatorの動作についてはコメントに書かれている通りです。日本語に訳すと「このモジュールをオブジェクトにミックスする(Ruby的に言うとextendする)とそのメソッド呼び出しはSinatra::Applicationクラスのメソッド呼び出しに移譲される」となります。

Delegatorメソッドで行われている処理は以下のようになります。難解なのは「moduleでselfってどういうこと?」ということだと思います。module~endの間でのselfはモジュールオブジェクトになります。

  • target属性の定義と初期値設定
    • attr_accessorを使用し、target属性(ゲッターメソッドとセッターメソッド)を定義する。「class << self」とあるがここではクラスとは関係なくメソッドが実行されるコンテキストをself(=Delegatorモジュール)にするという意味。つまり、Delegatorモジュールにクラス変数的1にtarget属性が追加される。このような書き方になっているのはattr_accessorがprivateメソッド(レシーバを指定してのメソッド呼び出しができないメソッド)のため。
    • targetの初期値としてApplicationクラス(Sinatra::Application)を設定。
  • delegateメソッドの定義と実行
    • delegateメソッドを定義する。これは「self.delegate」なので*Delegatorモジュールの特異メソッド(クラスで言うとクラスメソッド)*になる。このメソッドはextendしてもオブジェクトにメソッドは追加されない。
    • 定義したdelegateメソッドを呼び出す。delegateメソッドではdefine_methodを使い「targetで設定されたクラスのメソッドを呼び出す移譲メソッド」が定義されている。つまり、例えばgetメソッドを呼び出すとSinatra::Applicationクラスのgetクラスメソッドが呼び出されるようになる。なおここで定義されるメソッドはextend時にオブジェクトに追加される。

トップレベル(グローバル)でルーティングを定義するのだから裏側にオブジェクトがいてそいつに登録されるのだろうなとは思ってましたが、予想よりもかなり複雑なことをしているなという印象です。
さて次はgetメソッドが定義されているSinatra::Applicationですね。と言いたいところですが、Applicationはあまり処理が書かれておらず、Sinatraの本体と言えるのはその親クラスのSinatra::Baseです。

Sinatra::Base(登録処理周り)

Sinatra::Baseは長い(ほぼ1000行)のでこの記事ではルーティング登録に関する部分のみを説明します。それ以外の部分については起動編、処理編で説明します。

getメソッドは以下のようになっています。routeメソッドでルーティングが設定されているのだろうなと予測できます。

sinatra/base.rbより抜粋
    class << self
      # Defining a `GET` handler also automatically defines
      # a `HEAD` handler.
      def get(path, opts = {}, &block)
        conditions = @conditions.dup
        route('GET', path, opts, &block)

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

routeメソッド(念のためですが、これ以降もすべて特異メソッド=クラスメソッドです)。compile!メソッドを呼び出して処理を行ったうえで@routesに追加しています。@routesにはHTTPメソッド(動詞)ごとに処理が登録されているということがわかります。

sinatra/base.rbより抜粋
      def route(verb, path, options = {}, &block)
        enable :empty_path_info if path == "" and empty_path_info.nil?
        signature = compile!(verb, path, block, options)
        (@routes[verb] ||= []) << signature
        invoke_hook(:route_added, verb, path, block)
        signature
      end

compile!に進む。ちなみにSinatraコードの特徴として、メソッドに「!」を付けているものが多く、Rubyのお決まり的には「オブジェクト状態が変わるメソッドは!を付ける」だと思いますが、読んでる分にinternal的な意味で付けているように思います。

sinatra/base.rbより抜粋
      def compile!(verb, path, block, **options)
        # Because of self.options.host
        host_name(options.delete(:host)) if options.key?(:host)
        # Pass Mustermann opts to compile()
        route_mustermann_opts = options.key?(:mustermann_opts) ? options.delete(:mustermann_opts) : {}.freeze

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

        pattern                 = compile(path, route_mustermann_opts)
        method_name             = "#{verb} #{path}"
        unbound_method          = generate_method(method_name, &block)
        conditions, @conditions = @conditions, []
        wrapper                 = block.arity != 0 ?
          proc { |a, p| unbound_method.bind(a).call(*p) } :
          proc { |a, p| unbound_method.bind(a).call }

        [ pattern, conditions, wrapper ]
      end

      def compile(path, route_mustermann_opts = {})
        Mustermann.new(path, mustermann_opts.merge(route_mustermann_opts))
      end

compileメソッドではパターンマッチングに使うMustermannオブジェクトが作成されています。今回の本題ではないのでこちらは置いといて、generate_methodに進みます。

sinatra/base.rbより抜粋
      def generate_method(method_name, &block)
        define_method(method_name, &block)
        method = instance_method method_name
        remove_method method_name
        method
      end

書いてあるままですが何をしているかというと、

  1. 渡されたブロック(getメソッドに渡されるブロック。つまり、パスに対する処理)を「インスタンスメソッドとして」定義する。
  2. 定義したメソッド(メソッドオブジェクト)を取得する。なおcompile!での受け取り側変数が示すようにここで取得されるのはUnboundMethod(特定のインスタンスに結び付いていないメソッド)
  3. 定義したメソッドを削除する。なお「クラスのインスタンスメソッド一覧」から削除されるだけで一つ前で「オブジェクト参照」が取得されているのでガーベジコレクトされることはありません。ちなみに削除しているのは「一覧からは消す」ためと思われます。

最後にcompile!の一部を再掲します。ここで作られているprocが実行されるのは実際にパスに対するリクエストが来たときですが、unboundなメソッドにインスタンスを関連付けて実行しているということが推測できます。

compile!の一部
        wrapper                 = block.arity != 0 ?
          proc { |a, p| unbound_method.bind(a).call(*p) } :
          proc { |a, p| unbound_method.bind(a).call }

うーんだいぶややこしい。「ブロックをインスタンスのコンテキストで実行してるのかな」と思ってはいたのですが、それを実現するにはこんな風に書かないといけないのですね・・・

ここまでのまとめ

以上ここまでルーティングの登録処理について見てきました。「普通のオブジェクト」以外のselfについて理解することがここまでを読み解く鍵と言えるでしょう。

  • DSLで使うメソッドはSinatra::Delegatorに定義されており2、mainオブジェクトがSinatra::Delegatorをextendすることでトップレベルに定義される。
  • module~end内のselfはモジュールオブジェクトになる。このselfを利用し、モジュールオブジェクトにクラス変数的に属性を定義したり、クラスメソッド的にメソッドを定義したりできる。
  • 渡されたブロックはSinatra::Baseクラスのインスタンスメソッドとして登録される(ただしメソッド一覧からはすぐに削除される)。リクエスト処理時にインスタンスをバインドして実行すると推測される。

起動編へ続く。

  1. 文法的にはクラス変数ではなく「モジュールオブジェクト(インスタンス)のインスタンス変数」ですが実質的にはクラス変数みたいなものです。

  2. 続く記事で触れますが全部のメソッドがSinatra::Delegatorの仕組みで動いているわけではありません。

4
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?