Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New cop to suggest replacing case/when with hash lookup #8247

Closed
fatkodima opened this issue Jul 6, 2020 · 13 comments · Fixed by #8280
Closed

New cop to suggest replacing case/when with hash lookup #8247

fatkodima opened this issue Jul 6, 2020 · 13 comments · Fixed by #8280

Comments

@fatkodima
Copy link
Contributor

When case/when represents a simple 1:1 mapping, suggest to replace it with a hash lookup.

# bad
case country
when "europe"
  "http://eu.example.com"
when "america"
  "http://us.example.com"
end

# good
SITES = {
  "europe"  => "http://eu.example.com",
  "america" => "http://us.example.com"
}
SITES[country]

I'm propose to name it Style/CaseHashMapping (is there a better name?). Maybe it would be wise to add a Min config option, specifying for how many branches should be to consider an offense.

@bbatsov Interested in your thoughts on this.

@bbatsov
Copy link
Collaborator

bbatsov commented Jul 6, 2020

I love the idea! As for the name -> something like HashLikeCase or StaticMappingViaCase sound reasonably good to me, although not great. Naming is hard! 😆

@fatkodima
Copy link
Contributor Author

Cool, will implement it then 😄

@marcandre
Copy link
Contributor

Should this be in rubocop-performance? Or is there another goal here?
Will that be limited to string values?

@fatkodima
Copy link
Contributor Author

Keys can be strings and symbols, values can be any [recursive]_basic_literal.
Yes, it will be faster, but the main goal is to make the code easier. So I believe this should go to the Style department here.

@koic
Copy link
Member

koic commented Jul 6, 2020

Hm. Intuitively, this rule seems to belong to Performance department. I think Style can provide the opposite option, but it doesn't seem to be the case.

Also, I think that conversion to Hash constant is limited only when Hash values have the same type.

@tejasbubane
Copy link
Contributor

The correction can be without variable:

{
  "europe"  => "http://eu.example.com",
  "america" => "http://us.example.com"
}[country]

@rrosenblum
Copy link
Contributor

The inline version that @tejasbubane suggested would be the easiest to implement with auto-correct. I think extracting a constant to the correct location within the file will prove to be tricky. However, I do think using a hash would be a better solution.

I also agree with others that this sounds like a good candidate for performance rather than style. Do we have any metrics to back this up?

@marcandre
Copy link
Contributor

The correction can be without variable:

{
  "europe"  => "http://eu.example.com",
  "america" => "http://us.example.com"
}[country]

Building a hash is expensive, this is a bad idea

@fatkodima
Copy link
Contributor Author

fatkodima commented Jul 8, 2020

Benchmark

# frozen_string_literal: true

require 'bundler/inline'

gemfile(true) do
  gem 'benchmark-ips'
end

SITES = {
  "europe"  => "http://eu.example.com",
  "america" => "http://us.example.com",
  "australia" => "http://au.example.com"
}

def case_when(country)
  case country
  when "europe"
    "http://eu.example.com"
  when "america"
    "http://us.example.com"
  when "australia"
    "http://au.example.com"
  end
end

def constant_hash(country)
  SITES[country]
end

def inline_hash(country)
  {
    "europe"  => "http://eu.example.com",
    "america" => "http://us.example.com",
    "australia" => "http://au.example.com"
  }[country]
end

Benchmark.ips do |x|
  x.report('case-when')     { case_when(SITES.keys.sample) }
  x.report('constant hash') { constant_hash(SITES.keys.sample) }
  x.report('inline hash')   { inline_hash(SITES.keys.sample) }
  x.compare!
end

Results

Comparison:
       constant hash:  4367232.1 i/s
           case-when:  4263872.0 i/s - same-ish: difference falls within error
         inline hash:  3369430.3 i/s - 1.30x  (± 0.00) slower

Still looks like a stylistic cop to me.

@mame
Copy link
Contributor

mame commented Jul 22, 2020

Just FYI: As @fatkodima's benchmark suggests, MRI applies the similar optimization internally. For every trivial case/when statement, the interpreter builds an internal hash from literal keys to bytecode addresses at the compilation, and lookups the hash and just jumps during execution. So, I think this is just a matter of coding styles.

@edsu
Copy link

edsu commented Dec 30, 2021

But why is a hash considered better style than a case? I personally find the case style much more readable, but what am I missing?

@bbatsov
Copy link
Collaborator

bbatsov commented Dec 30, 2021

Well, there's always subjectivity but if something is essentially a dictionary-structure it's usually a good idea to represent it as such (meaning a hash in Ruby). For me this reduces the cognitive overhead.

@edsu
Copy link

edsu commented Dec 30, 2021

That's a little bit tautological, but it seems you aren't alone in feeling that way :-) luckily I can always turn it off.

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

Successfully merging a pull request may close this issue.

8 participants