タイトルを読んでも何を言っているのかわからないと思いますが、モジュールを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 end
はmodule M : Object end
のshorthandです。
このshorthandを解決すると、この2つのコードは同じ形になります。そのため、この2つのエラーの原因は同じです。
では、このコロンはどういう意味を表しているのでしょうか?
module M : Object end
と定義したモジュールM
は、Object
相当のクラスにinclude
できます。
言い換えると、モジュールM
の中ではObject
に定義されたメソッド(例えばObject#tap
)を使えます。
module M2 :C end
も同様で、このモジュールM2
はC
相当のクラスにinclude
できます。
ではなぜObject
クラスにinclude M
するとエラーになるのでしょうか。include
先として指定しているのだから、include
できると考えるのが直感的です。
この原因は、このままではObject
の定義が解決できないためです。
では、RBSの気持ちになってObject
の定義の解決を試みてみましょう。
この定義では、Object
にはM
モジュールがinclude
されています。つまりObject
の定義を解決するには、先にM
モジュールの定義を解決する必要があります。
ところで、M
モジュールの定義を解決するには、Object
の定義を解決する必要があります。なぜならば、M
モジュールはObject
クラス相当にinclude
できるモジュールであり、Object
クラスがどんなインターフェイスかを知る必要があるためです。
……と、ここでObject
とM
の定義の解決でループができてしまいました。
今回のエラーはこのループを指摘するものです。つまり、無事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
まとめ
ちょっと分かりづらくて解決がむずいのではないかなと思って記事にしました。
Object
にinclude
するモジュールはあまり書くことはないと思いますが、1つのクラスにしかinclude
しないモジュールはRailsを書いていると見かけることがある(1つのモデルにしかinclude
しないconcernとか)ので、Railsアプリケーションに型をつけているとこのエラーに出会うことがあるかもしれません。そのときにはこの記事を参考にして頂ければ幸いです。