Reputation: 3672
Since there is no type in ruby, how do Ruby programmers make sure a function receives correct arguments? Right now, I am repeating if object.kind_of
/instance_of
statements to check and raise runtime errors everywhere, which is ugly. There must be a better way of doing this.
Upvotes: 25
Views: 18508
Reputation: 3621
Using raise
for Manual Type Checking
You can manually check the type of parameters and raise an error if they are incorrect.
def my_method(param)
raise TypeError, "Expected String, got #{param.class}" unless param.is_a?(String)
puts "Valid input: #{param}"
end
my_method("Hello") # Works fine
my_method(123) # Raises: TypeError: Expected String, got Integer
Using respond_to?
for Duck Typing
Instead of checking for a specific class, you can check if the object responds to a required method.
def my_method(param)
unless param.respond_to?(:to_str)
raise TypeError, "Expected a string-like object, got #{param.class}"
end
puts "Valid input: #{param}"
end
my_method("Hello") # Works fine
my_method(:symbol) # Raises TypeError
Using Ruby 3's Type Signatures (rbs)
Ruby 3 introduced RBS and TypeProf for static type checking.
Define types in an RBS file (.rbs):
def my_method: (String) -> void
Using sorbet for Stronger Type Checking
Sorbet is a third-party static type checker.
require 'sorbet-runtime'
extend T::Sig
sig { params(param: String).void }
def my_method(param)
puts "Valid input: #{param}"
end
my_method("Hello") # Works fine
my_method(123) # Raises error at runtime
Refer:
https://github.com/sorbet/sorbet
https://railsdrop.com/2025/02/13/type-checking-and-type-casting-in-ruby/
Upvotes: 0
Reputation: 1037
You can use a Design by Contract approach, with the contracts ruby gem. I find it quite nice.
Upvotes: 1
Reputation: 3287
I recommend to use raise at the beginning of the method to add manual type checking, simple and effective:
def foo(bar)
raise TypeError, "You called foo without the bar:String needed" unless bar.is_a? String
bar.upcase
end
Best way when you don't have much parameters, also a recommendation is to use keyword arguments available on ruby 2+ if you have multiple parameters and watch for its current/future implementation details, they are improving the situation, giving the programmer a way to see if the value is nil.
plus: you can use a custom exception
class NotStringError < TypeError
def message
"be creative, use metaprogramming ;)"
#...
raise NotStringError
Upvotes: 2
Reputation: 168199
My personal way, which I am not sure if it a recommended way in general, is to type-check and do other validations once an error occurs. I put the type check routine in a rescue block. This way, I can avoid performance loss when correct arguments are given, but still give back the correct error message when an error occurs.
def foo arg1, arg2, arg3
...
main_routine
...
rescue
## check for type and other validations
raise "Expecting an array: #{arg1.inspect}" unless arg1.kind_of?(Array)
raise "The first argument must be of length 2: #{arg1.inspect}" unless arg1.length == 2
raise "Expecting a string: #{arg2.inspect}" unless arg2.kind_of?(String)
raise "The second argument must not be empty" if arg2.empty?
...
raise "This is `foo''s bug. Something unexpected happened: #{$!.message}"
end
Suppose in the main_routine
, you use the method each
on arg1
assuming that arg1
is an array. If it turns out that it is something else, to which each
is not defined, then the bare error message will be something like method each not defined on ...
, which, from the perspective of the user of the method foo
, might be not helpful. In that case, the original error message will be replaced by the message Expecting an array: ...
, which is much more helpful.
Upvotes: 24
Reputation:
Ruby is, of course, dynamically typed.
Thus the method documentation determines the type contract; the type-information is moved from the formal type-system to the [informal type specification in the] method documentation. I mix generalities like "acts like an array" and specifics such as "is a string". The caller should only expect to work with the stated types.
If the caller violates this contract then anything can happen. The method need not worry: it was used incorrectly.
In light of the above, I avoid checking for a specific type and avoid trying to create overloads with such behavior.
Unit-tests can help ensure that the contract works for expected data.
Upvotes: 19
Reputation: 146123
If a method has a reason to exist, it will be called.
If reasonable tests are written, everything will be called.
And if every method is called, then every method will be type-checked.
Don't waste time putting in type checks that may unnecessarily constrain callers and will just duplicate the run-time check anyway. Spend that time writing tests instead.
Upvotes: 9