Reputation: 4259
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]}
?
Upvotes: 7
Views: 9940
Reputation: 2518
Ruby 2.7 added a shorthand block syntax where you can use _1
, _2
etc to access the params passed to a block, so you can do something like this:
arr.map { _1[:a] }.reduce(:+)
Edit:
In Ruby 3.4 (which is in preview at the time of writing) you can use it
instead of _1
:
arr.map { it[:a] }.reduce(:+)
https://bugs.ruby-lang.org/issues/18980
Upvotes: 2
Reputation: 11281
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.
Upvotes: 0
Reputation: 96914
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
Upvotes: 4
Reputation: 806
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] }
Upvotes: 0