Skip to content

Instantly share code, notes, and snippets.

@agrberg
Last active January 7, 2020 19:13
Show Gist options
  • Save agrberg/2b7374a352ab9fbd17a604c9639fa9d3 to your computer and use it in GitHub Desktop.
Save agrberg/2b7374a352ab9fbd17a604c9639fa9d3 to your computer and use it in GitHub Desktop.

Hash and Keyword Coercion

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.

My Impressions and Recommendations

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.

@zaynehz
Copy link

zaynehz commented Jan 7, 2020

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?

@zaynehz
Copy link

zaynehz commented Jan 7, 2020

Bold indicates what I believe to be typos

"Ruby has only had keyword arguments since version 2.0" -> "Ruby introduced keyword arguments in version 2.0" (Might need to verify that that's accurate, I didn't look it up)

"so it has allowed hashes to be written implicitly as arguments without the {}s for quite some time" -> Does this imply that they allowed implicit arguments without {}s before the introduction of keyword arguments or that they didn't allow it before the introduction?

(I don't know which is correct but I might change the wording, depending)

"This behavior also works in..." - I think this could use some clarity around what behavior you're referencing. The ability to write arguments implicitly? I would maybe even swap it with the next line so you get something like:

"Ruby introduced keyword arguments in version 2.0, [idk what to put here yet to replace so it has allowed hashes to be written implicitly as arguments without the {}s for quite some time]. If a method takes keyword arguments and a hash is passed, it is implicitly broken down into keyword arguments. 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. This behavior also works in the other direction."

@zaynehz
Copy link

zaynehz commented Jan 7, 2020

On a more structural note, I think the first part could be divided into more clearly delineated sections of discussion:

  1. introduction to the problem (the two approaches to passing hash arguments)
  2. discussion of the first approach (passing a hash) - example, benefits, potential consequences
  3. discussion of the second approach (passing arguments implicitly) - example, benefits, potential consequences

@zaynehz
Copy link

zaynehz commented Jan 7, 2020

"What's more, this opens up potentially hard to understand code when determining if to-hash or to-keyword coercion will happen."

(I might actually even reword this as "Additionally, this can increase barriers to code readability for developers trying to determine if to-hash or to-keyword coercion will happen."

@zaynehz
Copy link

zaynehz commented Jan 7, 2020

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

This feels a little out of the blue - I think some leading discussion of why this change is being made is missing? Changing the structure to more clearly discuss each approach may help clarify where this is coming from, especially if you have the approach that they're moving towards discussed directly before this and maybe a small section about "So what does this mean for developers?"

@zaynehz
Copy link

zaynehz commented Jan 7, 2020

"For example, ____ takes an options hash for its behavior and an html_options hash for customizing the tag as its second and third arguments respectively. "

Should there be something in the blank there? What takes an options hash?

@zaynehz
Copy link

zaynehz commented Jan 7, 2020

Overall I think an interesting discussion. I think there's room to maybe personalize it a little with an example of a) how it affected a team at RigUp and b) how we handled it. I think it also takes for granted some context that isn't included in the article, which is okay but it might benefit from a brief summary of that context (What is Ruby changing? Why is the current state of things an issue) directly at the beginning of the article instead of nearer the midpoint.

Since this is a very conversational/discussion-focused article, you might experiment with shuffling stuff around into a structure more like:

  1. Introduction: what is the summary of the context of this conversation and why is it important?
  2. Discuss in depth the differences, pros and cons between the two approaches. Consider adding a personal anecdote from RigUp in this section to tie it back to the company blog
  3. Discuss the upcoming changes in Ruby and the benefits they offer, as well as a brief summary of how to prepare for the changes (you already have this)
  4. Conclusion: your takeaways and plans based on this information (basically your last paragraph already)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment