Hello. Lately I have been playing with the type system in Ruby and I notice some patterns that cannot be represented by the RBS syntax... So I wanted to start a discussion about it. I hope we can get some fruitful insights.
(OBS.: The discussion here is based on the example about splatting in the syntax document and one of my doubts in the issue in soutaro/steep#160.)
Motivation
In the syntax doc, it seems that the type of rest arguments can only be uniform (i.e. the same type for all the entries)...
So in the following example it seems that the current grammar does not allow writing <something> in such a way that the arguments given to lazy are type matched against the arguments in the signature or @callable (please correct me if I am wrong)
class Effect
def initialize(callable)
@callable = callable
end
def lazy(*args)
-> { @callable.(*args) }
end
end
module MyModule
def self.func(a, b)
a * b
end
end
# @type var effect: ^() -> Array[String | Integer]
effect = Effect.new(MyModule.method(:func)).lazy([1, "a"] , 2)
puts "Effect: #{effect.()}" # => [1, "a", 1, "a"]
# ------------------- .rbs ---------------------
class Effect[<something>, S]
def initialize: (^(*<something>) -> S) -> void
def lazy: (*<something> args) -> (^() -> S)
end
interface _Prod
def *: (Numeric) -> _Prod
end
module MyModule
def self.func: (_Prod, Numeric) -> _Prod
end
Proposal
Based on this my proposal would be change the meaning of the type associated with the splat operator to refer to the entire array of arguments instead of each argument individually (i.e. make T in (*T) -> S mean the type of the array instead of the type of each individual element).
Please notice that with this change we can represent both uniform and heterogeneous lists of arguments (if someone wants to enforce uniform types, it can be done with (*Array[T]) -> S). Without the grammar is somehow incompatible with the main use of the splat operator nowadays (heterogeneous types).
Please notice the suggestion is also valid for keyword arguments: T in (**T) -> S would represent the entire type of the hash of keywords, not the type of each keyword value. If someone wants to enforce uniformity in the type, that could be done with `(**Hash[Symbol, T]) -> S.
Summary of the proposal:
|
nowadays |
with the proposed change |
| uniform rest args |
(*A, **B) -> C |
(*Array[A], **Hash[Symbol, B]) -> C |
| heterogeneous rest args |
--impossible-- |
(*A, **B) -> C |
Other implementations/languages
By the documentation, Sorbet doesn't seem to support heterogeneous rest args either, and apparently this decision was inspired by Scala. However Ruby syntax is different from Scala in that respect (disclaimer: I might be wrong here, I am not a Scala programmer). Apparently, according to this website, Scala prohibits the un-splatting of heterogeneous lists, and instead provides an alternative method to obtain virtually the same effect. This is not the case of Ruby... un-splatting of heterogeneous arrays is super-common (and the standard way we forward args between method calls), and I don't remember now any existing alternative for un-splatting...
Therefore, I believe taking inspiration from Scala is not a good call on this matter.
We can instead look on how our friends in the Python community are dealing with this, since the grammar for splats in Python and Ruby is almost identical. Indeed they also opted by using uniform rest args, however this decision created a series of issues, and difficulties to produce type signatures even inside the standard library, that they are currently struggling to solve. Particularly it is impossible to annotate complex decorators - one of the pillars of Python's high-order functions - that modify/hide/augment the argument list of the decorated functions -- please notice that the decorator pattern in Python is similar to the pattern of wrapping blocks in Ruby/passing blocks around. The following links represent some of the issues pointed out by the Python community and the attempts to solve the limitation (mostly based on providing an alternative construct interpreted by the type checker).
It seems that main contributors of Python's reference type checker do appreciate that heterogeneous rest args are important and are debating for an alternative instrument to represent it, other than directly in the type annotation grammar to avoid breaking retro-compatibility. However, since the type grammar for stubs is Ruby is not consolidated yet, I see this as a great opportunity, so we can adopt a grammar that can handle both use cases (heterogeneous and uniform) and be more future proof.
Hello. Lately I have been playing with the type system in Ruby and I notice some patterns that cannot be represented by the RBS syntax... So I wanted to start a discussion about it. I hope we can get some fruitful insights.
(OBS.: The discussion here is based on the example about
splattingin the syntax document and one of my doubts in the issue in soutaro/steep#160.)Motivation
In the syntax doc, it seems that the type of rest arguments can only be uniform (i.e. the same type for all the entries)...
So in the following example it seems that the current grammar does not allow writing
<something>in such a way that the arguments given tolazyare type matched against the arguments in the signature or@callable(please correct me if I am wrong)Proposal
Based on this my proposal would be change the meaning of the type associated with the splat operator to refer to the entire array of arguments instead of each argument individually (i.e. make
Tin(*T) -> Smean the type of the array instead of the type of each individual element).Please notice that with this change we can represent both uniform and heterogeneous lists of arguments (if someone wants to enforce uniform types, it can be done with
(*Array[T]) -> S). Without the grammar is somehow incompatible with the main use of the splat operator nowadays (heterogeneous types).Please notice the suggestion is also valid for keyword arguments:
Tin(**T) -> Swould represent the entire type of the hash of keywords, not the type of each keyword value. If someone wants to enforce uniformity in the type, that could be done with `(**Hash[Symbol, T]) -> S.Summary of the proposal:
(*A, **B) -> C(*Array[A], **Hash[Symbol, B]) -> C(*A, **B) -> COther implementations/languages
By the documentation, Sorbet doesn't seem to support heterogeneous rest args either, and apparently this decision was inspired by Scala. However Ruby syntax is different from Scala in that respect (disclaimer: I might be wrong here, I am not a Scala programmer). Apparently, according to this website, Scala prohibits the
un-splattingof heterogeneous lists, and instead provides an alternative method to obtain virtually the same effect. This is not the case of Ruby...un-splattingof heterogeneous arrays is super-common (and the standard way we forward args between method calls), and I don't remember now any existing alternative for un-splatting...Therefore, I believe taking inspiration from Scala is not a good call on this matter.
We can instead look on how our friends in the Python community are dealing with this, since the grammar for splats in Python and Ruby is almost identical. Indeed they also opted by using uniform rest args, however this decision created a series of issues, and difficulties to produce type signatures even inside the standard library, that they are currently struggling to solve. Particularly it is impossible to annotate complex decorators - one of the pillars of Python's high-order functions - that modify/hide/augment the argument list of the decorated functions -- please notice that the decorator pattern in Python is similar to the pattern of wrapping blocks in Ruby/passing blocks around. The following links represent some of the issues pointed out by the Python community and the attempts to solve the limitation (mostly based on providing an alternative construct interpreted by the type checker).
It seems that main contributors of Python's reference type checker do appreciate that heterogeneous rest args are important and are debating for an alternative instrument to represent it, other than directly in the type annotation grammar to avoid breaking retro-compatibility. However, since the type grammar for stubs is Ruby is not consolidated yet, I see this as a great opportunity, so we can adopt a grammar that can handle both use cases (heterogeneous and uniform) and be more future proof.