Amethyst の動的な部分の処理方法を調べてみた

[前回の記事](http://qiita.com/pacuum/items/35b7b7f3bf3002c4d98f)ではAmethystをインストールして Hello World を試してみました。もう少し具体的な処理を実装して見る前に、Amethystがどうやって動的な処理を実行してみるのか調べてみることにしました。

crystal は静的型付のコンパイル型言語です。一方で Amethyst は rails と同様に、ユーザの入力に応じて Controller とその action を動的に dispatch します。Controller は crystal の中でオブジェクトとして操作できるのでまだ実装方法の想像は付きますが、crystal には Method クラスはありませんのでメソッドをオブジェクトとして操作することはできません。どうやっているのでしょうか？

## コード
まず、controller と app のコードを見ておきます。

```rb
class HelloApp < Base::App
  routes.draw do
    get "/world", "hello#world"
    register HelloController
  end
end

class HelloController < Base::Controller
  actions :world

  def world
    html "hello world!"
  end
end
```
まず App では `register`というmacro?によって controller を何処かに登録をしているようです。また controller クラスの中でも `actions` という macro でアクションを登録しているっぽいですね。
現時点では全ての controller と action に対してこの登録処理をしないといけないようです。ちょっと面倒ですね。action の登録はmacroでどうにか自動化できそうなの気もするのでそのうち記述しなくてよくなるのではないでしょうか。

## register
では register が何をしているのか追ってみましょう。これはlibs/amethyst/amethyst/dispatch/router.cr に定義されています。

```rb
  # Adds controller to hash @controllers and make it available for app
  def register(controller)
    register_controller controller
  end

  # @controllers.fetch("ControllerName").new
  macro register_controller(klass_name)
    @controllers["#{{{klass_name.id}}}"] = {{klass_name.id}}
  end
```
`register` がインスタンスメソッドで、その中では `register_controller` でした。この、一旦メソッドを挟んでマクロを呼ぶというスタイルは何度か見かけたのですが、これは macro をインスタンスメソッドの様に呼べない（macro def というものはあるけど引数の処理が異なる）制限を回避するテクニックではないかと思います。

このコード自体がやっていることは controller 名の文字列から controller クラスオブジェクトへの hash を作成しているということになります。これを作っておけば動的にクラスを特定するのは難しくないですね。

ただ個人的にはmacroのどこまでがコンパイル時に実行されるのかまだ良く把握できていないのでイマイチ挙動が正確に理解できていません。

## actions
次は `actions` です。

```rb
  macro actions(*actions)
    private def add_actions
      {% for action in actions %}
        @actions[{{action}}.to_s] = ->{{action.id}}
      {% end %}
    end
  end
```
これは macro ですね。与えられたアクション名を使って、action から proc への hash を作っています。proc の中では action をそのまま引数無しで呼び出しているだけです。crystal には Method クラスは無いですし、send も reflection も無いのですが、このように proc を噛ますことで send に似た動的な処理が実現できるということですね。なるほど！

ただしこれはやり過ぎるとせっかく型付き言語にした意味がないので多用すべきテクニックでは無い気がしますね。このようなことが可能なのは `Hash (String,Proc)` が Proc の引数や戻り値の型までチェックしないという緩さによるものです。また、今回のようにどの action も引数を持たない/同一の引数を持つような場合にのみ使えるテクニックだと思います。

なお、ここでは `add_actions` というメソッドを定義する形をとっているので、 actions を二度に分けて呼び出すことはできません。controller の中の action の数が多くなってくるとちょっと面倒な気はします。

## Dispatch
では実際に dispatch している箇所のコードを見てみます。

```rb
# Process regular routes (which are in @routes)
def process_named_route(request : Http::Request, response : Http::Response)
  controller = @matched_route.controller
  controller_instance = @controllers_instances[controller] ||=  @controllers.fetch(controller).new
  controller_instance.set_env(request, response)
  response = @controllers_instances[controller].call_action(@matched_route.action)
end
```

想像どおりのコードです。route に定義された文字列からクラス名の文字列と action 名の文字列を取り出して、まず controller の hash から controller クラスを取り出し、controller クラスから action proc を取り出すということをしています。`call_action` メソッドはフィルタを実行してから proc を call するだけでした。納得です。

ただ一つ気になるのは controller のインスタンスを使いまわしていることです。controller ではインスタンス変数に値を格納することがあるので、前のリクエストのデータが残っていると予期しないバグの原因になったりメモリリークの原因になるのでは？という気がしますね。今後の改善に期待です。

## まとめ
crystal でも proc を間に挟むことによって send のような reflection を限定的に実行できることがわかりました。個人的にはこのようなフレームワーク部分など以外では使うべきではないテクニックだとは思います。