96

I have a simple array:

arr = ["apples", "bananas", "coconuts", "watermelons"]

I also have a function f that will perform an operation on a single string input and return a value. This operation is very expensive, so I would like to memoize the results in the hash.

I know I can make the desired hash with something like this:

h = {}
arr.each { |a| h[a] = f(a) }

What I'd like to do is not have to initialize h, so that I can just write something like this:

h = arr.(???) { |a| a => f(a) }

Can that be done?

8 Answers 8

143

Say you have a function with a funtastic name: "f"

def f(fruit)
   fruit + "!"
end

arr = ["apples", "bananas", "coconuts", "watermelons"]
h = Hash[ *arr.collect { |v| [ v, f(v) ] }.flatten ]

will give you:

{"watermelons"=>"watermelons!", "bananas"=>"bananas!", "apples"=>"apples!", "coconuts"=>"coconuts!"}

Updated:

As mentioned in the comments, Ruby 1.8.7 introduces a nicer syntax for this:

h = Hash[arr.collect { |v| [v, f(v)] }]
Sign up to request clarification or add additional context in comments.

8 Comments

I think you meant ... { |v| [v, f(v)] }, but this did the trick!
Just one thing - why's there a * next to *arr.collect?
@Jeriko - the splat operator * collects a list into an array or unwinds an array into a list, depending on context. Here it unwinds the array into a list (to be used as the items for the new hash).
After looking at Jörg's answer and thinking this over some more, note that you can remove both * and flatten for a simpler version: h = Hash[ arr.collect { |v| [ v, f(v) ] } ]. I'm not sure if there's a gotcha I'm not seeing, however.
In Ruby 1.8.7, the ugly Hash[*key_pairs.flatten] is simply Hash[key_pairs]. Much nicer, and require 'backports' if you haven't updated from 1.8.6 yet.
|
61

Did some quick, dirty benchmarks on some of the given answers. (These findings may not be exactly identical with yours based on Ruby version, weird caching, etc. but the general results will be similar.)

arr is a collection of ActiveRecord objects.

Benchmark.measure {
    100000.times {
        Hash[arr.map{ |a| [a.id, a] }]
    }
}

Benchmark @real=0.860651, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.8500000000000005, @total=0.8500000000000005

Benchmark.measure { 
    100000.times {
        h = Hash[arr.collect { |v| [v.id, v] }]
    }
}

Benchmark @real=0.74612, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=0.740000000000002, @total=0.750000000000002

Benchmark.measure {
    100000.times {
        hash = {}
        arr.each { |a| hash[a.id] = a }
    }
}

Benchmark @real=0.627355, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=0.6199999999999974, @total=0.6299999999999975

Benchmark.measure {
    100000.times {
        arr.each_with_object({}) { |v, h| h[v.id] = v }
    }
}

Benchmark @real=1.650568, @cstime=0.0, @cutime=0.0, @stime=0.12999999999999998, @utime=1.51, @total=1.64

In conclusion

Just because Ruby is expressive and dynamic, doesn't mean you should always go for the prettiest solution. The basic each loop was the fastest in creating a hash.

3 Comments

You, my friend, are awesome for doing your homework and posting it :)
Slightly faster to use a manually incremented loop variable: I don't have your dataset - I just cooked a trivial object with an @id accessor and more-or-less matched your numbers - but direct iteration shaved a couple of % off. Stylistically, I prefer {}.tap { |h| .... } to assigning a hash, because I like encapsulated chunks.
I'm a little confused at the difference between arr.map and arr.collect considering that .collect is merely an alias to .map
46

Ruby 2.6.0 enables a shorter syntax by passing a block to the to_h method:

arr.to_h { |a| [a, f(a)] }

Comments

35
h = arr.each_with_object({}) { |v,h| h[v] = f(v) }

2 Comments

This reads a lot more concisely than using Hash[arr.collect{...}]
This is incredibly slow, check out my post below: stackoverflow.com/a/27962063/1761067
11

This is what I would probably write:

h = Hash[arr.zip(arr.map(&method(:f)))]

Simple, clear, obvious, declarative. What more could you want?

2 Comments

I like zip as much as the next guy, but since we're already calling map, why not leave it at this? h = Hash[ arr.map { |v| [ v, f(v) ] } ] Is there an advantage to your version that I'm not seeing?
@Telemachus: With all the Haskell code I've been reading, I just got used to point-free programming, that's all.
6

I'm doing it like described in this great article http://robots.thoughtbot.com/iteration-as-an-anti-pattern#build-a-hash-from-an-array

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h.merge(fruit => f(fruit)) }

More info about inject method: http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-inject

1 Comment

This does a merge for every step of the iteration. Merge is O(n), as is the iteration. Thus, this is O(n^2) while the problem itself is obviously linear. In absolute terms, I just tried this on an array with 100k elements and it took 730 seconds, while the other methods mentioned in this thread took anywhere from 0.7 to 1.1 seconds. Yes, that's a slowdown by Factor 700!
3

in addition to the answer of Vlado Cingel (I cannot add a comment yet, so I added an answer).

Inject can also be used in this way: the block has to return the accumulator. Only the assignment in the block returns the value of the assignment, and an error is reported.

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h[fruit]= f(fruit); h }

1 Comment

I benchmarked the two versions: the use of merge doubles the execution time. The above inject version is a comparabe to the collect version of microspino
1

Another one, slightly clearer IMHO -

Hash[*array.reduce([]) { |memo, fruit| memo << fruit << f(fruit) }]

Using length as f() -

2.1.5 :026 > array = ["apples", "bananas", "coconuts", "watermelons"]
 => ["apples", "bananas", "coconuts", "watermelons"] 
2.1.5 :027 > Hash[*array.reduce([]) { |memo, fruit| memo << fruit << fruit.length }]
 => {"apples"=>6, "bananas"=>7, "coconuts"=>8, "watermelons"=>11} 
2.1.5 :028 >

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.