Ruby offers a sometimes confusing flexibility with the mercurial difference between keyword arguments and arguments that are hashes. For example, the following two calls are equivalent:
https://gist.github.com/4cc3f5d600ab9ea54b7dae0fd8c1e774
Ruby didn't always have keyword arguments so it allows hashes to be written implicitly as arguments without the {}
s. This behavior also works in the other direction. If a method takes keyword arguments and a hash is passes, it is implicitly broken down into keyword arguments. Note that this will trigger an ArgumentError
if one of the hash's keys is not a known keyword value just as if you passed an unexpected keyword.
https://gist.github.com/5ac2fc99e9c2c273cd5d543d99097f5c
One important overlooked aspect is that while mixed keys will work with the to-hash coercion:
https://gist.github.com/db4bf12dfb5c1af8957549c3849f3026
it only works with all symbol keys for the to-keyword coercion:
https://gist.github.com/24affc700fd4b9bdd6d43aceddd522b9
Whats more, this opens up potentially hard to understand code when determining if to-hash or to-keyword coercion will happen.
https://gist.github.com/e63ed4194e335224d956feff25a33cbf
Now that keyword arguments are being used more often it can be confusing. Ruby 2.7 will start adding deprecation warnings for confusing syntax that will be removed in Ruby 3. For example, calling baz
with an implicit hash as seen in the first call above will return the warning
warning: Passing the keyword argument as the last hash parameter is deprecated
You can find all of the warnings and suggestions on how to future proof your code quickly in the tl;dr
section of the official Ruby Language blog post.
I feel this is a good idea that will result in code whose meaning is absolutely clear on first read.
I learned Ruby via Rails so this behavior has always been a little more obvious as it's common for view layer methods to take multiple hashes as positional arguments. For example, link_to
takes an options
hash for its behavior and an html_options
hash for customizing the tag as its second and third arguments respectively. Calls like the following are quite common:
https://gist.github.com/2dbc255f8090c9d295dd3777913f3c5f
Interestingly, the above call will only issue a warning if the method signature uses keyword arguments.
Ultimately this is another case where being explicit reduces the cognitive load on developers instead of relying on your entire team to remember all the possibilities, edge cases, and that all trailing non curly brace wrapped key-value pairs will be coerced into a hash.
For example, at first glance you cannot tell if func(one: 1, two: 2)
takes a single hash positional argument like foo
or keyword arguments like bar
. However, the method definitions will be obvious from the following calls in Ruby 3:
https://gist.github.com/f80a88d16eb1985f3151009ec5151354
Hash coercion will still remain in Ruby and some will still strongly prefer foo(one: 1, two: 2)
as it's in The Ruby Style Guide. Maybe that will change with Ruby 3 but you and your team can always decide for yourselves as Rubocop's Style/BracesAroundHashParameters
cop is configurable. Finally, foo(**{one: 1, two: 2})
also will work as intended but I'd hope your team would flag it in code review for being misleading.
The prose is too purple in the first paragraph imo - suggest: "The flexibility offered by the difference between positional hash arguments and keyword arguments can sometimes be confusing. For example, the following two calls are equivalent:"
Might also add something like "Flexibility can offer powerful tools and extensibility, but sometimes that extensibility is just enough rope to hang yourself on" at the beginning, depending on what you're trying to convey?