Proposal: wildcard method member `...` for classes with undeclared methods

オープン
#3,266 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
15/100
issue の種類
機能追加
明瞭さ
おおむね明確
活発さ
活発
技術スタック
ruby
領域
devtools

調査の方向性

This is an open design proposal with no comments or decision yet, so agree on the semantics before writing code. The issue names DefinitionBuilder (keep the :"..." method in Definition#methods and add a fallback accessor), rbs validate and DefinitionBuilder (reject ... inside interface), rbs test (skip it), and docs/syntax.md. Done means those semantics are settled by the maintainers and each listed change is covered by tests.

索引モデルが issue の本文から書いたものです。

説明

Motivation

RBS requires every method of a class to be listed. For some classes that is impossible, and for others it is not worth doing yet, even though the class itself is well known:

  • Classes whose methods are defined by metaprogramming (define_method over a schema, attribute DSLs, ActiveSupport::Concern). The methods exist at runtime, but RBS has no way to say "this class has more methods than the ones written here", so every call is a NoMethod error unless all of them are enumerated by hand or by a generator.
  • Inherently dynamic classes such as OpenStruct and Hashie::Mash, whose member set is runtime data.
  • Classes whose methods could be enumerated, but where doing so is not worth it yet. With def `...`: (?) -> untyped a class can be typed gradually: declare the methods that matter, leave the rest untyped, and narrow later by adding defs. Today the only options are "list everything" or "nothing".

In the first two cases we usually know the type of the unknown members (untyped, Hashie::Mash, String, ...) but not their names; in the third we know the names and choose not to write them yet. Opening the class does not help with either: it is global, and it still needs the names.

This differs from #422, which asks for forwarding to another object whose method names are known.

Proposal

Give a special meaning to a method named ...: it is the type of every method that is not declared.

class Config
  def initialize: (Hash[Symbol, untyped]) -> void
  def `...`: () -> String                  # any other method returns String
end

class Hashie::Mash
  def `...`: () -> Hashie::Mash            # mash.foo
           | (untyped) -> untyped          # mash.foo = 1
end

class OpenStruct
  def `...`: (?) -> untyped                # arguments and result unknown
end

class Model
  def self.`...`: (*untyped) -> Array[Model]   # singleton side
end

Semantics:

  • Looking up an undeclared method m on the instance (or singleton) definition resolves to the method types of .... Arguments and blocks are checked against them as usual.
  • A declared method always wins. Adding a def narrows the type; the wildcard covers only what is not written. No compatibility between declared methods and the wildcard is required, since RBS lookups are always by a static name.
  • ... propagates like any method: to subclasses, through include/prepend (instance side) and extend (singleton side), and can be overridden.
  • ... does not participate in subtyping. A class with def `...`: () -> String does not satisfy an interface requiring def name: () -> String; otherwise such a class would be a subtype of almost everything.
  • ... is not allowed in interface. An interface describes which names an object responds to; "and unknown others" has no meaning there. This is permanent, not a v1 limitation.

No new syntax is required

Backtick-quoted method names already accept .... On current master (4.3.0.dev):

Input Result
def `...`: () -> String parses, name == :"...", kind == :instance
def self.`...`: () -> String parses, kind == :singleton
def `...`: () -> A | (?) -> untyped parses, overloads work; round-trips through RBS::Writer
def ...: () -> String (bare) syntax error (pDOT3)

So the change is purely semantic: parser, writer, C and Rust are untouched. :"..." becomes a reserved method name, in the same way DefinitionBuilder already treats initialize specially to synthesize new. Ruby cannot def a method named ... (syntax error), so nothing real collides. Because the bare form is currently an error, it can be added as sugar later without compatibility concerns.

Changes in rbs

  • DefinitionBuilder: keep :"..." in Definition#methods so inheritance, mixins and overriding work unchanged; add an accessor such as Definition#fallback_method so consumers need not know the magic name.
  • Reject def `...` inside interface in rbs validate and DefinitionBuilder (it currently parses).
  • rbs test: skip :"..." instead of trying to hook it.
  • Document the member in docs/syntax.md.

For type checkers (informative, Steep as an example): on lookup failure, use ... instead of reporting NoMethod; hide it from completion and from MethodDefinitionMissing.

Non-goals

  • Forwarding to another object (#422): needs the target's names, separate feature.
  • Name-dependent types (:name → String, :age → Integer): ... discards the name. If wanted later, typing method_missing is the natural place, and it composes with this proposal.
  • Closing the wildcard in a subclass: RBS cannot remove methods either.

Alternatives considered

  • A class-level annotation like %a{incomplete} ("unknown methods are (?) -> untyped"). Needs the same DefinitionBuilder work plus new propagation rules, and cannot express a result type. def `...`: (?) -> untyped covers it.
  • Using the declared type of method_missing. For metaprogrammed classes method_missing is never called, and BasicObject#method_missing exists everywhere, so an opt-in would still be needed.
  • Code generation (rbs prototype runtime, rbs_rails, orthoses). Complementary; this is for when names cannot be computed or generation is not worth it.

Prior art with the same "unknown name, known type" shape: TypeScript index signatures (which likewise do not satisfy named property requirements), Haxe Dynamic<T>, Python __getattr__ -> T, Swift @dynamicMemberLookup.

主要言語
Ruby
スター
2.2k
フォーク
260
平均マージ
22時間 3分
マージ済み PR(30日)
54

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

ruby/rbs のほかの issue

ruby/rbs の issue をすべて見る

似ている issue

Ruby の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。