pythoniku
pythoniku

Reputation: 3672

How do Ruby programmers do type checking?

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

Answers (6)

abhilashak
abhilashak

Reputation: 3621

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

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

  1. 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
  1. 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://sorbet.org/

https://github.com/sorbet/sorbet

https://railsdrop.com/2025/02/13/type-checking-and-type-casting-in-ruby/

  • Use is_a? or respond_to? for runtime type checking.
  • Use Ruby 3’s RBS for static type enforcement.
  • Use Sorbet for stricter runtime type checks.

Upvotes: 0

Julien
Julien

Reputation: 1037

You can use a Design by Contract approach, with the contracts ruby gem. I find it quite nice.

Upvotes: 1

makevoid
makevoid

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

sawa
sawa

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

user166390
user166390

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

DigitalRoss
DigitalRoss

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

Related Questions