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

Refactor Hash#transform_values and Hash#transform_values! #16149

Merged
merged 2 commits into from
Jul 12, 2014

Conversation

sferik
Copy link
Contributor

@sferik sferik commented Jul 12, 2014

This patch makes two small changes to the new Active Support core extensions Hash#transform_values and Hash#transform_values!:

  1. These methods now return an Enumerator if no block is given. This makes them more consistent with other Enumerable methods and allows for method chaining, for example:

    {a: 1, b: 2, c: 3}.transform_values.with_index{|x, i| [x * x, i]}
    #=> {:a=>[1, 0], :b=>[4, 1], :c=>[9, 2]}

    Before this patch, such chaining would result in a LocalJumpError: no block given.

  2. Since the &block parameter is never referenced, I’ve removed it from Hash#transform_values. This prevents a garbage Proc object from being constructed automatically on method invocation, resulting in measurably faster performance. Here is the benchmark I ran:

    require 'benchmark/ips'
    
    class Hash
      def slow(&block)
        return enum_for(:slow) unless block_given?
        result = self.class.new
        each do |key, value|
          result[key] = yield(value)
        end
        result
      end
    
      def fast
        return enum_for(:fast) unless block_given?
        result = self.class.new
        each do |key, value|
          result[key] = yield(value)
        end
        result
      end
    end
    
    HASH = {a: 1, b: 2, c: 3}
    
    Benchmark.ips do |x|
      x.report("slow") { HASH.slow { |x| x * x } }
      x.report("fast") { HASH.fast { |x| x * x } }
    end
    slow 388660.2 (±10.6%) i/s - 1931275 in 5.030443s
    fast 621865.9 (±11.5%) i/s - 3085425 in 5.029155s
    

References #15819. cc/ @guilleiguaran @rafaelfranca @sgrif

@matthewd
Copy link
Member

Should we have a test for the enum_for behaviour? What's the precedent in our other Enumerable extensions?

@sferik
Copy link
Contributor Author

sferik commented Jul 12, 2014

I should also mention that I considered refactoring Hash#transform_values to:

def transform_values(&block)
  dup.transform_values!(&block)
end

This would be more DRY but it also incurs a performance penalty (from duping) that I decided was not a worthwhile tradeoff.

require 'benchmark/ips'

class Hash
  def slow(&block)
    dup.transform_values!(&block)
  end

  def fast(&block)
    return enum_for(:fast) unless block_given?
    result = self.class.new
    each do |key, value|
      result[key] = yield(value)
    end
    result
  end

  def transform_values!
    return enum_for(:transform_values!) unless block_given?
    each do |key, value|
      self[key] = yield(value)
    end
  end
end

HASH = {a: 1, b: 2, c: 3}

Benchmark.ips do |x|
  x.report("slow") { HASH.slow { |x| x * x } }
  x.report("fast") { HASH.fast { |x| x * x } }
end
slow 297345.7 (±10.5%) i/s - 1473507 in 5.009120s
fast 396055.9 (±11.1%) i/s - 1967210 in 5.030581s

@sferik
Copy link
Contributor Author

sferik commented Jul 12, 2014

@matthewd Regardless of precedent, I’d argue that returning an Enumerable is the better behavior. I’ll gladly update Hash#transform_keys and similar methods to make them all consistent. I’ll also happy add tests for this behavior.

@matthewd
Copy link
Member

@sferik oh, yes, I was just wondering about precedent for whether we bother writing a test for it; the behaviour itself is desirable.

I believe the dup-based spelling, which was abandoned for performance, was the source of the unused '&block' wart, incidentally.

@schneems
Copy link
Member

Great work 👍

@sferik
Copy link
Contributor Author

sferik commented Jul 12, 2014

@matthewd I’ve added a couple tests and update the other extensions to Hash for consistency.

@@ -6,7 +6,8 @@ class Hash
# hash.transform_keys{ |key| key.to_s.upcase }
# # => {"NAME"=>"Rob", "AGE"=>"28"}
def transform_keys
result = {}
return enum_for(:transform_keys) unless block_given?
result = self.class.new
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like it would have implications in the vicinity of HWIA...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please state those implications in the form of a failing test?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was more just expressing subtle surprise that we didn't already have a test that expected HWIA#transform_keys to return a plain Hash. I have no actual objection to the more consistent behaviour.

@sgrif
Copy link
Contributor

sgrif commented Jul 12, 2014

LGTM

guilleiguaran added a commit that referenced this pull request Jul 12, 2014
Refactor Hash#transform_values and Hash#transform_values!
@guilleiguaran guilleiguaran merged commit 84b4260 into rails:master Jul 12, 2014
@sferik sferik deleted the refactor-transform_values branch July 12, 2014 16:29
sferik added a commit to sferik/rails that referenced this pull request Jul 12, 2014
matthewd added a commit that referenced this pull request Jul 12, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants