arr.each_with_object(Hash.new(0)) { |(*a,n),h| h[a] += n }.map(&:flatten)
#=> [["A", "Red", 15], ["B", "Red", 3], ["B", "Blue", 5], ["C", "Blue", 3],
# ["C", "Black", 1], ["D", nil, 9]]
Первый шаг вычисления:
h = arr.each_with_object(Hash.new(0)) { |(*a,n),h| h[a] += n }
#=> {["A", "Red"]=>15, ["B", "Red"]=>3, ["B", "Blue"]=>5,
# ["C", "Blue"]=>3, ["C", "Black"]=>1, ["D", nil]=>9}
Используется форма Ха sh :: new , которая принимает аргумент, называемый значением по умолчанию. Все это означает, что когда парсер Ruby расширяется h[a] += 1
до
h[a] = h[a] + n
h[a]
, справа возвращается значение по умолчанию h
, 0
, если h
делает нет ключа a
. Например, когда h
пусто,
h[["A", "Red"]] = h[["A", "Red"]] + 7 #=> 0 + 7 => 7
h[["A", "Red"]] = h[["A", "Red"]] + 8 #=> 7 + 8 => 15
h
не имеет ключа ["A", "Red"]
в первом выражении, поэтому h[["A", "Red"]]
справа возвращает значение по умолчанию, 0
тогда как h
имеет этот ключ во втором выражении, поэтому значение по умолчанию не применяется.
h.map(&:flatten)
является сокращением для
h.map { |a| a.flatten }
Когда переменная блока a
имеет значение установить равным первой паре ключ-значение h
,
a #=> [["A", "Red"], 15]
Итак
a.flatten
#=> ["A", "Red", 15]
Чтобы понять |(*a,n),h|
, нам нужно построить перечислитель
enum = arr.each_with_object(Hash.new(0))
#=> #<Enumerator: [["A", "Red", 7], ["A", "Red", 8], ["B", "Red", 3],
# ["B", "Blue", 2], ["B", "Blue", 3], ["C", "Blue", 3],
# ["C", "Black", 1], ["D", nil, 4], ["D", nil, 5]]
# :each_with_object({})>
Теперь мы сгенерируем первое значение из перечислителя (используя Enumerator # next ) и присвоим значения переменным блока:
(*a,n),h = enum.next
#=> [["A", "Red", 7], {}]
a #=> ["A", "Red"]
n # => 7
h #=> {}
Способ возврата массива enum.next
разбивается на составляющие элементы, которые присваиваются переменным блока, называется декомпозиция массива . Это мощная и очень полезная техника.