rbs
rbs copied to clipboard
Proposal: making `Class` and `Module` classes generic -- `Class[I]` and `Module[I]`
Currently, we model module/class themselves (as opposed to instance
s) 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).
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.
Class[String]
cannot be equivalent tosingleton(String)
, becausesingleton(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 toallocate
method. (RBS generatesnew
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.
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
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
…
Another use case: https://rubydoc.info/gems/ffi/1.16.3/FFI/StructByReference
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
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.)
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
.
I found this is already syntactically valid in RBS
Right... I forgot it...