49

RuboCop suggests:

Use Array.new with a block instead of .times.map.

In the docs for the cop:

This cop checks for .times.map calls. In most cases such calls can be replaced with an explicit array creation.

Examples:

# bad
9.times.map do |i|
  i.to_s
end

# good
Array.new(9) do |i|
  i.to_s
end

I know it can be replaced, but I feel 9.times.map is closer to English grammar, and it's easier to understand what the code does.

Why should it be replaced?

Drenmi
  • 8,492
  • 4
  • 42
  • 51
shirakia
  • 2,369
  • 1
  • 22
  • 33
  • 5
    Note that it's in the "Performance" group (as you can see from your link), which is a bit of a hint. :-) – ruakh Jan 07 '17 at 07:50
  • All performance cops were adapted from: https://github.com/JuanitoFatas/fast-ruby :-) – Drenmi Jan 08 '17 at 08:31

3 Answers3

48

The latter is more performant; here is an explanation: Pull request where this cop was added

It checks for calls like this:

9.times.map { |i| f(i) }
9.times.collect(&foo)

and suggests using this instead:

Array.new(9) { |i| f(i) }
Array.new(9, &foo)

The new code has approximately the same size, but uses fewer method calls, consumes less memory, works a tiny bit faster and in my opinion is more readable.

I've seen many occurrences of times.{map,collect} in different well-known projects: Rails, GitLab, Rubocop and several closed-source apps.

Benchmarks:

Benchmark.ips do |x|
  x.report('times.map') { 5.times.map{} }
  x.report('Array.new') { Array.new(5){} }
  x.compare!
end
__END__
Calculating -------------------------------------
           times.map    21.188k i/100ms
           Array.new    30.449k i/100ms
-------------------------------------------------
           times.map    311.613k (± 3.5%) i/s -      1.568M
           Array.new    590.374k (± 1.2%) i/s -      2.954M

Comparison:
           Array.new:   590373.6 i/s
           times.map:   311612.8 i/s - 1.89x slower

I'm not sure now that Lint is the correct namespace for the cop. Let me know if I should move it to Performance.

Also I didn't implement autocorrection because it can potentially break existing code, e.g. if someone has Fixnum#times method redefined to do something fancy. Applying autocorrection would break their code.

michaelrbock
  • 1,160
  • 1
  • 11
  • 20
mikdiet
  • 9,859
  • 8
  • 59
  • 68
  • 1
    Yeah, that actually seems like a good idea. It pre-allocates an array of the correct size. `times` gives no such heads up to `map` so it has to guess and potentially resize. – tadman Jan 07 '17 at 07:50
  • 1
    Please don't just post a link, summarize the linked content in a paragraph. – akuhn Jan 07 '17 at 10:35
  • And the link doesn't even explain why it is slower. – akuhn Jan 07 '17 at 10:40
  • The question was "Why it should be replaced", not "why it is slower". Following the best practices does not mean full understanding of underling reasons. @akuhn – mikdiet Jan 07 '17 at 11:06
  • 4
    In 2 years, rubocop might have been moved to another repo, and the link might be dead. We're all here to educate ourselves : there's a difference between not understanding everything, and not even trying to understand anything. – Eric Duminil Jan 07 '17 at 11:20
  • 1
    "Following the best practices does not mean full understanding of underling reasons" <--- this is totally untrue – micapam Jul 10 '18 at 03:35
16

If you feel it is more readable, go with it.

This is a performance rule and most codepaths in your application are probably not performance critical. Personally, I am always open to favor readability over premature optimization.

That said

100.times.map { ... }
  • times creates an Enumerator object
  • map enumerates over that object without being able to optimize, for example the size of the array is not known upfront and it might have to reallocate more space dynamically and it has to enumerate over the values by calling Enumerable#each since map is implemented that way

Whereas

Array.new(100) { ... }
  • new allocates an array of size N
  • And then uses a native loop to fill in the values
Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
akuhn
  • 27,477
  • 2
  • 76
  • 91
  • 3
    _**"If you feel it is more readable go with it."**_ Thank you! Plus, if you look at @Drenmi's answer, he shows that the `times` is only 60% slower than `Array`. If it was 1000% slower, then it might sway your decision. But these kinds of minor performance differences would be outweighed by long-term Human readability. – Joshua Pinter Jan 20 '19 at 21:33
6

When you need to map the result of a block invoked a fixed amount of times, you have an option between:

Array.new(n) { ... }

and:

n.times.map { ... }

The latter one is about 60% slower for n = 10, which goes down to around 40% for n > 1_000.

Note: logarithmic scale!

enter image description here

Drenmi
  • 8,492
  • 4
  • 42
  • 51