What is the correct RBS description for the following Ruby method:
def insert( id:, **options )
# ...
'Hello World'
end
Assuming the following usage:
insert( id: 123, a: 'A', b: 'B' )
CodePudding user response:
TL;DR
The RBS grammar doesn't always help tell you what a signature might accept given some arbitrary call. It just tells you what the signature actually defines, and potentially what a method returns. So, trying to define an RBS structure based on how you use your signature rather than the actual signature definition may not yield the results you expect.
Using RBS or TypeProf to Generate a Signature
RBS
Since your posted #insert method is not defined as part of a class, RBS will assume it's part of Object. Thus, if you put the contents of your original post into foo.rb and invoke rbs prototype rb foo.rb
, the signature for the method as interpreted by RBS will be:
class Object
def insert: (id: untyped, **untyped options) -> "Hello World"
end
Note that RBS v2.5.0 is pretty smart, and is telling you clearly that you haven't defined the type of either the :id
keyword, nor the names or types of values that the **options keyword arguments will accept. That's because you haven't defined them as part of the signature itself.
However, it also knows that the method returns a String with a known value, and tells you so.
TypeProf
TypeProf v0.21.2 works a little differently, and interprets the signature slightly differently, too. Given just the method above, it will return:
# Classes
class Object
private
def insert: (id: untyped, **untyped) -> String
end
where the primary differences are that it thinks this method is private, and just tells you that it returns a String rather than annotating the specific String it currently returns. However, if you expand foo.rb to include your method call as well as the method definition as follows:
def insert(id:, **options)
'Hello World'
end
insert(id: 123, a: 'A', b: 'B')
then TypeProf will attempt to infer the types based on the actual calls made to the method:
# Classes
class Object
private
def insert: (id: Integer, **String) -> String
end
The main different here is that TypeProf uses the single provided example of how you're calling the method to infer the expected types that :id
and the collected keyword arguments are expected to take. In your posted example, :id
takes an Integer, your collected keyword arguments are all String objects, and the method still returns a String although TypeProf doesn't assume the content of that String as rbs prototype
does.
If you call your method in multiple ways, rbs prototype
won't change but TypeProf will adjust the inferred types of the rbs signature based on the types passed by its various callers. For example, if you call your #insert method twice with different arguments:
insert id: 123, a: 'A', b: 'B'
insert id: 'foo', a: 1, b: [], c: {}
now TypeProf thinks the signature is:
# Classes
class Object
private
def insert: (id: Integer | String, **Array[untyped] | Hash[untyped, untyped] | Integer | String) -> String
end
TypeProf now thinks that :id
can expect either an Integer or a String, and that your currently-collected keyword options can take Array, Hash, and Integer values.
Both are Correct
From a grammar standpoint, all three results are valid RBS. It's up to you as the author to determine which tool best represents your intent, or whether you need to tune the RBS grammar to more accurately reflect what you expect the signature to be.
In addition, each tool has different modes of operation and output, and interprets Ruby code in different ways. So, you can't really treat one as better than the other so long as they can both output valid RBS grammar that can be validated with an RBS grammar validation tool.
Given your specific example, I think rbs prototype rb
gives a simpler and more accurate result, but TypeProf is likely to provide more of what you seem to expect the RBS grammar to provide (specifically, an explicit or duck-typed signature) without having to make manual changes to the RBS grammar file.
Explicit Signatures Yield More Explicit RBS
Of course, given more explicit signatures will help out both rbs and TypeProf. For example, running them the same way as above on the following code with a more explicit method definition gives you more specificity in the grammar:
def insert(id: 1, a: '', b: '')
'Hello World'
end
insert id: 123, a: 'A', b: 'B'
# rbs prototype rb foo.rb
class Object
def insert: (?id: ::Integer, ?a: ::String, ?b: ::String) -> "Hello World"
end
# typeprof foo.rb
class Object
private
def insert: (?id: Integer, ?a: String, ?b: String) -> String
end
Because your method signature is now more explicit, both rbs prototype
and typeprof
now return approximately the same result. Your signature is also be more clearly documented, since you're explcitly defining what keyword arguments are expected rather than leaving it up to a splatted collection of unknown keyword arguments.
At present, the RBS grammar isn't meant to take the place of YARD tags like @param. If you want that level of documentation without enforcement use YARD. If you want to enforce signature contracts or non-duck typing, then you need to use the RBS grammar to validate your code and enforce types with Steep, Sorbet, or other tools within the Ruby ecosystem. Matz has said Ruby is not expected to become a typed language internally, and therefore relies on external tools to enforce typing when desired.