Add type arguments support to singleton types
Add support for parameterized singleton types
This PR adds support for type parameters on singleton types to match the functionality available in Sorbet's T.class_of(X)[Y] syntax. With this change, RBS now supports the equivalent syntax: singleton(X)[Y].
Changes
- Updated the RBS grammar to allow type arguments for singleton types
- Modified the C parser implementation to accept type parameters for singleton types
Examples
Previously, only this was supported:
singleton(Array) # Class singleton type with no type parameters
Now this is also supported:
singleton(Array)[String] # Class singleton type with String type parameter
Questions
-
Should the
Applicationmodule be included in theClassSingletonclass, similar to how it's included inClassInstanceandInterface? Currently, I've implemented the necessary methods directly. -
Are there additional methods such as
map_typeandeach_typethat should be added to theClassSingletonclass?
@soutaro Did singleton(Array)[String] (or singleton(Array[String]), whatever) make sense in the first place?
@allcre Can you give me some examples why we need that type? I'm assuming singleton(Array) is a singleton type -- ::Array is the only one value of the type -- and we don't need generic type for it.
@soutaro we use it in Sorbet to represent the type of the attached class to a singleton.
It's useful around factories, here's a simple example:
class Box
extend T::Sig
extend T::Generic
E = type_member
sig { params(e: E).void }
def initialize(e)
@e = e
end
sig { returns(E) }
def e
@e
end
end
extend T::Sig
sig { returns(T.class_of(Box)[Box[Integer]]) }
def example
Box
end
x = example
T.reveal_type(x) # => T.class_of(Box)[Box[Integer]]
x.new("str") # error: Expected `Integer` but found `String` for argument `e`
More involved examples can be found in the documentation: https://sorbet.org/docs/class-of#tclass_of-applying-type-arguments-to-a-singleton-class-type.
RBS expects us to genericize class methods because it only recognizes type parameters at the instance level.
class Set[E]
def self.[]: [E] (*E elements) -> Set[E] # `E` is totally not duplicated
end
Honestly, that might be a flawed design:
class Set[E]
def self.[]: (*untyped elements) -> instance # `instance` is `Set[untyped]`!
end
Thanks @Morriar,
I got the use case and agree that it cannot be written in RBS now. Using an interface would be a workaround, but not sure if it can cover the existing use cases.
Let me confirm the semantics: the type singleton(T)[S, ...] means that a class object of T but the instance created through the value of the type is T[S, ...]. Looks like it makes some sense, except if it needs a big overhaul in RBS (and Steep).
What should we do for the other singleton methods?
class Box[T]
def initialize: (T) -> void
def self.new_array: [T] (T) -> Box[Array[T]]
end
b = Box #: singleton(Box)[String]
b.new("") # => Box[String]
b.new(1) # => type error
b.new_array(1) #=> ???
Looks like it only works for .new method in Sorbet?
What should we do for the other singleton methods?
How about changing the semantics so that type parameters apply on the class level as well? It would resolve my #1521.
class Box[T]
def self.new_array: (T) -> Box[Array[T]] # No need to genericize the method separately
def self.[]: (T) -> instance # `instance` was `Box[untyped]`, but now `Box[T]`
end
@ParadoxV5 Yeah, it would make sense. But, how can we associate the type parameter T in self.[] and the type parameter given to the singleton class. singleton(Box)[T]? Can we do that by adding another type instance[T]?
singleton(Box)[T] should probably be singleton(Box[T]) rather, so that singleton(Box[Integer])#[] → Box[Integer].[] → T in self.[] is Integer.
Allowing instance[T] is an option to, as I requested in #1521 long ago.
Its flexibility would enable def self.new_array: (T) -> instance[Array[T]].
Let me confirm the semantics: the type singleton(T)[S, ...] means that a class object of T but the instance created through the value of the type is T[S, ...].
Yes, when limited to the type of the attached class (instance in RBS?). Here's an example on sorbet.run.
But it can also be used to specify the type of the generic parameters to the singleton class:
class BoxFactory
class << self
extend T::Sig
extend T::Generic
Kind = type_member
sig { returns(Box[Kind]) }
def create
Box[Kind].new
end
end
end
class Box
extend T::Generic
Kind = type_member
end
extend T::Sig
sig do
type_parameters(:T)
.params(factory: T.class_of(BoxFactory)[BoxFactory, T.type_parameter(:T)])
.returns(Box[T.type_parameter(:T)])
end
def create_box(factory)
factory.create
end
Here's the equivalent RBS inline syntax we want with Sorbet:
class BoxFactory
#: [E]
class << self
#: -> Box[E]
def create
Box.new #: Box[E]
end
end
end
#: [E]
class Box
end
#: [T] (singleton(BoxFactory)[BoxFactory, T]) -> Box[T]
def create_box(factory)
factory.create
end