Skip to content

Add type arguments support to singleton types #2502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

allcre
Copy link
Contributor

@allcre allcre commented May 20, 2025

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

  1. Should the Application module be included in the ClassSingleton class, similar to how it's included in ClassInstance and Interface? Currently, I've implemented the necessary methods directly.

  2. Are there additional methods such as map_type and each_type that should be added to the ClassSingleton class?

@ParadoxV5
Copy link
Contributor

@soutaro Did singleton(Array)[String] (or singleton(Array[String]), whatever) make sense in the first place?

@allcre allcre force-pushed the Add_type_arguments_support_to_singleton_types branch 5 times, most recently from 06d9ce0 to 46c9aa2 Compare May 21, 2025 15:48
Previously, singleton type arguments could not be supported by RBS. This
adds support for them, enabling syntax like
`singleton(Array)[String, Integer]`.
@allcre allcre force-pushed the Add_type_arguments_support_to_singleton_types branch from 46c9aa2 to 91eafe8 Compare May 21, 2025 15:53
@allcre allcre marked this pull request as ready for review May 21, 2025 16:00
@soutaro
Copy link
Member

soutaro commented May 27, 2025

@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 soutaro self-assigned this May 27, 2025
@Morriar
Copy link
Contributor

Morriar commented Jun 3, 2025

@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.

@ParadoxV5
Copy link
Contributor

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

@soutaro
Copy link
Member

soutaro commented Jun 4, 2025

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?

@ParadoxV5
Copy link
Contributor

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

@soutaro
Copy link
Member

soutaro commented Jun 6, 2025

@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]?

@ParadoxV5
Copy link
Contributor

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]].

@Morriar
Copy link
Contributor

Morriar commented Jun 17, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants