pockestrap

Programmer's memo

RBSではmoduleのinclude先としてinclude先のクラスを指定してはいけない

タイトルを読んでも何を言っているのかわからないと思いますが、モジュールをincludeしようとするとRBS::RecursiveAncestorErrorが発生することがあります。 とくにObjectクラスにincludeするようなケースでは注意が必要です。

この記事ではrbs gem v1.0.0で動作確認しています。

Problem

次のtest.rbsを用意します。このRBSでは、Mモジュールを定義し、それをObjectクラスにincludeしているだけです。

# test.rbs
module M
end

class Object
  include M
end

このtest.rbsを置いたディレクトリでrbs validateを実行すると、次のエラーが発生してしまいます。

$ rbs -I . validate --silent
/path/to/lib/rbs/errors.rb:91:in `check!': /path/to/core/object.rbs:16:0...818:3: Detected recursive ancestors: ::Array[Elem] < ::Object < ::M < ::Object (RBS::RecursiveAncestorError)
    from /path/to/lib/rbs/definition_builder/ancestor_builder.rb:337:in `instance_ancestors'
    from /path/to/lib/rbs/definition_builder/ancestor_builder.rb:362:in `block in instance_ancestors'
    from /path/to/lib/rbs/definition_builder/ancestor_builder.rb:359:in `each'
    from /path/to/lib/rbs/definition_builder/ancestor_builder.rb:359:in `instance_ancestors'
    from /path/to/lib/rbs/definition_builder/ancestor_builder.rb:371:in `block in instance_ancestors'
    from /path/to/lib/rbs/definition_builder/ancestor_builder.rb:368:in `each'
    from /path/to/lib/rbs/definition_builder/ancestor_builder.rb:368:in `instance_ancestors'
    from /path/to/lib/rbs/definition_builder/ancestor_builder.rb:353:in `instance_ancestors'
    from /path/to/lib/rbs/definition_builder.rb:132:in `block in build_instance'
    from /path/to/lib/rbs/definition_builder.rb:731:in `try_cache'
    from /path/to/lib/rbs/definition_builder.rb:126:in `build_instance'
    from /path/to/lib/rbs/cli.rb:423:in `block in run_validate'
    from /path/to/lib/rbs/cli.rb:421:in `each_key'
    from /path/to/lib/rbs/cli.rb:421:in `run_validate'
    from /path/to/lib/rbs/cli.rb:113:in `run'
    from /path/to/exe/rbs:7:in `<top (required)>'
    from /home/pocke/.rbenv/versions/trunk/bin/rbs:23:in `load'
    from /home/pocke/.rbenv/versions/trunk/bin/rbs:23:in `<main>'

また次のような状態でも同様のエラーが発生します。 RBSのモジュール定義ではモジュール名の後にコロンを書いて、「そのモジュールがincludeされる先のインターフェイス」を指定できます。ところが、そこにinclude先のクラスを直接指定はできません。

module M2 : C
end

class C
  include M2
end
$ rbs -I . validate --silent
/path/to/rbs-1.0.0/lib/rbs/errors.rb:63:in `check!': test.rbs:2:0...3:3: Detected recursive ancestors: ::M2 < ::C < ::M2 (RBS::RecursiveAncestorError)
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:385:in `instance_ancestors'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:419:in `block in instance_ancestors'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:416:in `each'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:416:in `instance_ancestors'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:410:in `block in instance_ancestors'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:407:in `each'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:407:in `instance_ancestors'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder.rb:141:in `block in build_instance'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder.rb:765:in `try_cache'
    from /path/to/rbs-1.0.0/lib/rbs/definition_builder.rb:135:in `build_instance'
    from /path/to/rbs-1.0.0/lib/rbs/cli.rb:423:in `block in run_validate'
    from /path/to/rbs-1.0.0/lib/rbs/cli.rb:421:in `each_key'
    from /path/to/rbs-1.0.0/lib/rbs/cli.rb:421:in `run_validate'
    from /path/to/rbs-1.0.0/lib/rbs/cli.rb:113:in `run'
    from /path/to/rbs-1.0.0/exe/rbs:7:in `<top (required)>'
    from /home/pocke/.rbenv/versions/trunk/bin/rbs:23:in `load'
    from /home/pocke/.rbenv/versions/trunk/bin/rbs:23:in `<main>'

Cause

module M endmodule M : Object endのshorthandです。 このshorthandを解決すると、この2つのコードは同じ形になります。そのため、この2つのエラーの原因は同じです。

では、このコロンはどういう意味を表しているのでしょうか? module M : Object endと定義したモジュールMは、Object相当のクラスにincludeできます。 言い換えると、モジュールMの中ではObjectに定義されたメソッド(例えばObject#tap)を使えます。 module M2 :C endも同様で、このモジュールM2C相当のクラスにincludeできます。

ではなぜObjectクラスにinclude Mするとエラーになるのでしょうか。include先として指定しているのだから、includeできると考えるのが直感的です。 この原因は、このままではObjectの定義が解決できないためです。

では、RBSの気持ちになってObjectの定義の解決を試みてみましょう。 この定義では、ObjectにはMモジュールがincludeされています。つまりObjectの定義を解決するには、先にMモジュールの定義を解決する必要があります。 ところで、Mモジュールの定義を解決するには、Objectの定義を解決する必要があります。なぜならば、MモジュールはObjectクラス相当にincludeできるモジュールであり、Objectクラスがどんなインターフェイスかを知る必要があるためです。 ……と、ここでObjectMの定義の解決でループができてしまいました。

今回のエラーはこのループを指摘するものです。つまり、無事ObjectクラスにincludeできるMモジュールを定義するには、このループを解決しなければなりません。

Solution

Mモジュールのinclude先に、Object以外を指定すれば良いです。 たとえばObjectの親クラスであるBasicObjectや、適当なインターフェイスを指定すると良いでしょう。

BasicObjectを指定する例。この例では、MモジュールからはBasicObjectが持つメソッドしか使用できません。

# test.rbs
module M : BasicObject
end

class Object
  include M
end

Mモジュールに本当に必要なインターフェイスだけを指定する例。

# Object#tap 相当のメソッドだけを持ったインターフェイス
interface _Tappable
  def tap: () { (self) -> void } -> self
end

# モジュールMは _Tappable インターフェイスを
# 満たすクラス/モジュールにならincludeできる
module M : _Tappable
end

class Object
  include M
end

まとめ

ちょっと分かりづらくて解決がむずいのではないかなと思って記事にしました。

Objectincludeするモジュールはあまり書くことはないと思いますが、1つのクラスにしかincludeしないモジュールはRailsを書いていると見かけることがある(1つのモデルにしかincludeしないconcernとか)ので、Railsアプリケーションに型をつけているとこのエラーに出会うことがあるかもしれません。そのときにはこの記事を参考にして頂ければ幸いです。