crystal で maybe モナド風

タイトルはやや釣りです。

crystal には `try` メソッドが組み込みで実装されています。これについては説明の必要は無いと思います。また、ruby にはtryを拡張した [rmaybe](https://github.com/kmdsbng/rmaybe) というライブラリがあって、tryを何度も書かないといけないようなコードを簡潔に書くことができます。これは maybe モナドからインスピレーションを受けたライブラリですが地味に便利なのでよく使っています（method_missing を使っているのでやや遅いですが）。今日はこの rmaybe と同等のライブラリを crystal 実装するというネタで macro を勉強したいと思います。

# Goal
まずはどういうコードができればよいのかを明確にします。あまりややこしいのも面倒なのでシンプルに以下の様なコードが動作するように maybe という macro を定義します。

```rb
class A
  def a; self; end
  def b; self; end
  def c; self; end
end

def test(obj)
  maybe( obj.a.b.c ) #=> expanded to obj.try(&.a).try(&.b).try(&.c)
end

test A.new #=> instance of A
test nil #=> nil
```

# 調査
## macro 引数を調べる
とりあえず maybe macro を定義してみてどんなASTがもらえるのか、それに対して何ができそうなのかを見てみます。

```rb
macro maybe( expression )
 {% p expression %} #=> obj.a.b.c
 {% p expression.class_name %} #=> Call
end
```

`class_name` というのは ASTNode に定義されているメソッドです。一応説明しておくと、macro の中で動いているのは crystal 言語です。ただし、macro に引数として与えられるのはその字面である ASTノードです。これは [ASTNode](http://crystal-lang.org/api/Macros/ASTNode.html)を基底クラスとしたクラス階層を持っています。例えば 1 というリテラルは `NumberLiteral` クラスとなります。通常の Number ではないことに注意してください。両方にある程度同じメソッドは定義されていまて使用感は似ていますが同一ではありません。

さて、`obj.a.b.c` という引数を `maybe`マクロに渡した場合、`Call`というタイプのASTノードが渡されることがわかりました。これはメソッド呼び出しを表すASTです。`Call`クラスには `receiver`, `name`, `args` というメソッドが定義されていて、それぞれメソッドのレシーバ、メソッド名、引数を取得することができます。

```rb
macro maybe( expression )
 {% p expression.receiver %} #=> obj.a.b
 {% p expression.name %} #=> c
 {% p expression.args %} #=> []
end
```

この receiver が返す `obj.a.b` もまたメソッド呼び出しですので、これも Call オブジェクトになります。したがって、`expression.receiver.receiver` とすると `obj.a` を得ることができます。行けそうな感じがしてきましたね。

## 再帰？
このようなツリー構造に簡単にアクセスできることがわかったので後は macro で再帰が使えればOKなんですが、どうでしょうか。試しに以下の様な macro を定義すると segfault になります。

```rb
macro f
  f
end

f  
```

segfaultが起きるということは、無限ループに達したせいかもしれないので、今度は無限ループにならないような再帰を定義してみましょう。まずは再帰だけど実際には再帰呼び出しが一度も行われることのない場合。

```rb
macro f
  {% if false %}
    f
  {% end %}
end
f
```
無事コンパイルに通りました。次は１回だけ再帰呼び出しされる場合。

```rb
macro f( n )
  p {{n}}
  {% if n == 1 %}
    f(2)
  {% else %}
  {% end %}
end
f 1
```

これもOK。
つまりコンパイル時に再帰が必ず止まることを保証できればOKということですね。材料揃った感あります。

# 実装
後はそんなに難しくありません。渡されたASTがメソッド呼び出しだった場合はさらにmaybeを再帰呼び出しして、それ以外のときは`ASTNode`をそのまま返すようにしてやります。

```rb
macro maybe( expression )
 {% if expression.is_a?(Call) %}
   maybe({{expression.receiver}}).try(&.{{expression.name}})
 {% else %}
   {{expression}}
 {% end %}
end
```

コンパイルも無事通りました。

# 実行
実行してみます。

```bash
$ crystal run maybe.cr 
#<A:0x10e766fd0>
nil
```
予想どおりの動きになりました！

# でも・・・
では上記のクラス`A`の定義を、nil が返るかもしれないような定義にしてみましょう。

```rb
class A
  def a; self; end
  def b
    if true
      self
    else
      nil
    end
  end
  def c; self; end
end
```

こうするとコンパイルエラーが出ます。

```bash
$ crystal run a.cr 
Error in ./a.cr:28: undefined method 'c' for Nil (compile-time type is A?)
```
`b`が nil を返す可能性があることが型推論から推定されて、その結果 c の呼び出しに失敗しているということのようです。

ですが、実際に展開されるはずの式を直接コードに書いてみるとこれはコンパイルに通ります。

```rb
A.new.try(&.a).try(&.b).try(&.c)
```

`A.new.a.b.c`は上記のように展開されるはずなのですが、そのmacro展開をする前の式の時点で型チェックが動いてしまっているというように見えますね。ということで残念ですがまだこのコードは使えません。

# まとめ
macro を再帰的に呼び出してASTNodeを走査することでちょっと込み入った macro の定義ができることがわかりました。このようにかなりダイナミックかつ安全にコードの書き換えをすることが可能です。ただ、現状のバージョンでは型チェックのタイミングで問題が起きてしまうようです。