The Mythical Ruby Splat
Most Rubyists are aware of variable length arguments in a method definition. For example:
Just as a refresher, the * operator, commonly known as a “splat,” collects the arguments into an array object. So in the example above, the variable *names is an array object, which means it can be used just like any other array:
What blew my mind recently was stumbling across Ruby code where the name for the “splat” was missing. I assure you that this is valid Ruby code:
Kinda interesting, huh?
But when would you ever use this? Consider the slightly contrived example below:
AdminUser is a subclass of User using Active Record’s built-in single table inheritance.
Let’s assume that the user with ID of 1 is an instance of the User class and the user with ID of 42 is an instance of the AdminUser class:
Everything is hunky-dory until we need to change User#active?.
Let’s say requirements change and users are now active for a particular period. User#active? now takes a start date and end date to determine if the user is active during that period:
The problem is that our AdminUser#active? does not take any arguments and therefore will generate a runtime error if called with start_date and end_date:
We want AdminUser#active? to work just like User#active? but without updating the AdminUser class. Otherwise, every time we change the User class we’ll have to go and update the AdminUser class. The fix is to have originally defined AdminUser like so:
Having defined AdminUser#active? will cause it to safely ignore any arguments passed in. Going through the whole example again, User and AdminUser are defined as:
Now let’s redefine User#active? to take into account a start date and end date:
Everything works!
Using methods with an unnamed “splat” is probably something you won’t do too often, but it can be useful in defining your intentions.
Looking at the AdminUser class, the #active? method clearly defines that its arguments are not used versus other possible implementations like def active?(start_date, end_date),
def active?(*unused) or def active?(*_).
You can clearly see with active?(*) that the arguments provided will be ignored, whereas the other three you would need to dig into the implementation to verify.