6

I have an array of hashes:

arr = [ {:a => 1, :b => 2}, {:a => 3, :b => 4} ]

What I want to achieve is:

arr.map{|x| x[:a]}.reduce(:+)

but I think it's a bit ugly, or at least not that elegant as:

arr.map(&:a).reduce(:+)

The later one is wrong because there is no method called a in the hashes.

Are there any better ways to write map{|x| x[:a]}?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Xiao Jia
  • 4,169
  • 2
  • 29
  • 47
  • I think that is as good as it's going to get. I'm pretty sure hashes are not ordered in memory so it's only safe to get the keys you want. – squiguy Jun 04 '13 at 01:36
  • 1
    @squiguy Hashes preserve order in Ruby 1.9 and later. – Andrew Marshall Jun 04 '13 at 01:43
  • 3
    Trying to save six characters because it's a "bit ugly" is pretty questionable. – user229044 Jun 04 '13 at 02:08
  • 2
    @meagar And it might be argued that it *should* be ugly, as perhaps preferring Hashes over domain objects is itself questionable, and the pervasive “bit ugliness” throughout the code is a sign of over-use of Hashes. Maybe. – Andrew Marshall Jun 04 '13 at 02:10

3 Answers3

4

You could make actual Objects, possibly with a Struct:

MyClass = Struct.new :a, :b
arr = [MyClass.new(1, 2), MyClass.new(3, 4)]
arr.map(&:a).reduce(:+)  #=> 4

Or for more flexibility, an OpenStruct:

require 'ostruct'
arr = [OpenStruct.new(a: 1, b: 2), OpenStruct.new(a: 3, b: 4)]
arr.map(&:a).reduce(:+)  #=> 4

Of course either of these can be constructed from existing hashes:

arr = [{ :a => 1, :b => 2 }, { :a => 3, :b => 4 }]

ss = arr.map { |h| h.values_at :a, :b }.map { |attrs| MyClass.new(*attrs) }
ss.map(&:a).reduce(:+)  #=> 4

oss = arr.map { |attrs| OpenStruct.new attrs }
oss.map(&:a).reduce(:+)  #=> 4

Or, for a more creative, functional approach:

def hash_accessor attr; ->(hash) { hash[attr] }; end
arr = [{ :a => 1, :b => 2 }, { :a => 3, :b => 4 }]
arr.map(&hash_accessor(:a)).reduce(:+)  #=> 4
Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
  • 7
    `.map { |x| x[:a] }` remains the best by far :p – user229044 Jun 04 '13 at 02:08
  • @meagar Yea, though it’s fun to explore options. My comment above echoes the first (perhaps overshadowed) clause of my answer: make objects! – Andrew Marshall Jun 04 '13 at 02:12
  • @meagar Any way to rephrase that code? I tried `.map(&:[], :propname)`, but no alterations of that are working.. I just get syntax errors, even with `:send` in there as well... Pretzel colon!! Probably only accepts a single parameter.. – Pysis Dec 07 '17 at 16:47
  • @Pysis Why would you want to rephrase this? To what end? – user229044 Dec 07 '17 at 17:01
  • It just feels more concise without that `do` 'hash' block. – Pysis Dec 07 '17 at 23:31
0

It is unclear what you mean as "better" and why you think the correct version is ugly.

Do you like this "better"?

arr.inject(0) { |sum, h| sum + h[:a] }
Sergey Bolgov
  • 806
  • 5
  • 6
0

There's a way, extending the Symbol.

lib/core_extensions/symbol.rb (credit goes here)

# frozen_string_literal: true

class Symbol
  def with(*args, &)
    ->(caller, *rest) { caller.send(self, *rest, *args, &) }
  end
end

Then, given:

arr = [ {:a => 1, :b => 2}, {:a => 3, :b => 4} ]

you can do this:

arr.map(&:[].with(:a)).reduce(:+)

Explanation: to access hash value under any key, you call Hash#[] method. When passed as a :[] (extended) symbol to the Array#map, you can then call .with(*args) on this symbol, effectively passing the parameter (hash key) down to the :[] method. Enjoy.

wscourge
  • 10,657
  • 14
  • 59
  • 80