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

Abierto
#3,266 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
15/100
Tipo de issue
Nueva funcionalidad
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
ruby
Área
devtools

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
Ruby
Estrellas
2.2k
Forks
260
Merge medio
22 h 3 min
PR fusionados (30 d)
54

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de ruby/rbs

Todos los issues de ruby/rbs

Issues similares

Más issues de Ruby

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.