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

Aperta
#3,266 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
15/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
ruby
Ambito
devtools

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.

Lingua principale
Ruby
Stelle
2.2k
Fork
260
Merge medio
22h 3m
PR unite (30g)
54

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di ruby/rbs

Tutte le issue di ruby/rbs

Issue simili

Altre issue su Ruby

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.