rbs icon indicating copy to clipboard operation
rbs copied to clipboard

Proposal: making `Class` and `Module` classes generic -- `Class[I]` and `Module[I]`

Open ParadoxV5 opened this issue 1 year ago • 9 comments

Currently, we model module/class themselves (as opposed to instances) as singleton(Klass), whereas the Module/Class classes can only represent the module/class of untyped.

I suggest combining those two by giving Module and Class a type variable representing their instances. For example, Class[String] (and Module[String] too to under covariance) is equivlaent to singleton(String). For backward compatibility, singleton(I) can be a alternate (read: legacy) syntax for Module[I].

The immediate benefit is Class[I] RBS finally able to write def new: (…) -> I and def allocate: () -> I (rather than expecting type checkers to infer from singleton(I)).

Speaking of singleton(I) — https://github.com/ruby/rbs/blob/6e5a2893b4c2782cc9e71f76e193e9592b0c1ebf/docs/syntax.md?plain=1#L9 [I < Bound] … singleton(I) is invalid. [I < singleton(Bound)] -> I works the limitation around, but what if I has other duties?

# Modules can’t inherit Classes, so I’m stuck with this design.
class MyClassWrapper[I < MyObject] < MyComponent
  def initialize: (singleton(I) klass) -> void #FIXME: Class[I] when
  def customized_new: (*args) -> I
end

This is also how Java does it (as early as their type args were born).

ParadoxV5 avatar Sep 24 '23 00:09 ParadoxV5

Class[String] cannot be equivalent to singleton(String), because singleton(String) has singleton methods.

I'm not sure if adding a generic parameter to Class makes sense, because it can only be used to allocate method. (RBS generates new for each classes with types.)

I'm curious if there are more use cases.

soutaro avatar Sep 28 '23 13:09 soutaro

Class[String] cannot be equivalent to singleton(String), because singleton(String) has singleton methods.

You are correct, the singleton(String) syntax enables special handling of singleton methods on the String class. But also note that, anything can have singleton methods, not just classes (Class instances). And unfortunately, there is no RBS equivalent for:

class << SINGLETON = Superclass.new
  def my_singleton_method …

and the current workaround is:

SINGLETON: Superclass & _SINGLETON
interface _SINGLETON
  def my_singleton_method …

I'm not sure if adding a generic parameter to Class makes sense, because it can only be used to allocate method. (RBS generates new for each classes with types.)

I'm curious if there are more use cases.

class Class[I]
  # The attached objects of singleton classes is the only “instance” they can have,
  # vs. regular classes that have `#allocate`/`#new`
  def attached_object: () -> I
  # This is `[T < I] (Class[T]) -> void` with the ineffective `[T]` flattened.
  def inherited: (Class[I]) -> void
  # ditto
  def subclasses: () -> Array[Class[I]]
  # If RBS has explicit countervariance like Java does…
  def superclass: [I < T] () -> Class[T]
end

The top post also includes an abstraction of a user use case. Here’s my use case unabstracted.

ParadoxV5 avatar Sep 28 '23 18:09 ParadoxV5

If I may jump in, but i'd consider reserving that syntax to solve either module mixins, or delegation, both of which aren't yet solved in rbs.

module A[B]
  # A mixin of B, meaning methods of A can call functions defined in B

# or

class A[B]
  # class a delegates methods to B, could also solve the "delegate" stdlib

HoneyryderChuck avatar Sep 28 '23 22:09 HoneyryderChuck

If I may jump in

of coarse you may

but i'd consider reserving that syntax to solve either module mixins, or delegation, both of which aren't yet solved in rbs.

module A[B]
  # A mixin of B, meaning methods of A can call functions defined in B

# or

class A[B]
  # class a delegates methods to B, could also solve the "delegate" stdlib

Let type variables go wild and do crazy things –

module A[B]
  include B
  …

ParadoxV5 avatar Sep 28 '23 22:09 ParadoxV5

Another use case: https://rubydoc.info/gems/ffi/1.16.3/FFI/StructByReference

ParadoxV5 avatar Nov 18 '23 00:11 ParadoxV5

Another use case in resolv: https://github.com/ruby/rbs/pull/1655

We could type Resolv::DNS::Resource::Generic.create(65280, 1).new('data').data as String if we could write the following type definitions:

class Resolv::DNS::Resource::Generic < Resolv::DNS::Resource
  def self.create: (Integer type_value, Integer class_value) -> Class[Resolv::DNS::Resource::Generic]
  def initialize: (String data) -> void
  def data: () -> String
end

hanazuki avatar Nov 29 '23 09:11 hanazuki

I prefer extending generics upper bounds for resolv:

class Resolv::DNS::Resource::Generic < Resolv::DNS::Resource
  def self.create: [T < singleton(Generic)] (Integer type_value, Integer class_value) -> T
end 

(singleton(Generic) cannot be written as an upperbound for now.)

soutaro avatar Nov 30 '23 01:11 soutaro

class Resolv::DNS::Resource::Generic < Resolv::DNS::Resource
  def self.create: [T < singleton(Generic)] (Integer type_value, Integer class_value) -> T
end 

(singleton(Generic) cannot be written as an upperbound for now.)

Having played with RBS and Steep, I found this is already syntactically valid in RBS but is not yet supported by Steep for type checking. Is this right?

I thought singleton(C) was something related to C.new.singleton_class (that is a subtype of the type C), but it is actually C.singleton_class (the type of the value C). The following seem to be the most specific types that work with the current implementation of Steep.

class Resolv::DNS::Resource::Generic < Resolv::DNS::Resource
  def self.create: (Integer type_value, Integer class_value) -> singleton(Generic)
end

NB. singleton(C) < singleton(Generic) for any class C < Generic.

hanazuki avatar Nov 30 '23 08:11 hanazuki

I found this is already syntactically valid in RBS

Right... I forgot it...

soutaro avatar Dec 01 '23 03:12 soutaro