Может кто-нибудь объяснить, почему этот способ итерации вложенной структуры данных действительно работает?

1 Flip [2015-08-02 16:37:00]

Я хотел создать этот массив

["studies", "theory", "form", "animal", "basic", "processes"]

из следующей вложенной структуры данных (сохраненной как sorted_hash):

[["studies", {:freq=>11, :cap_freq=>0, :value=>11}],
 ["theory", {:freq=>9, :cap_freq=>1, :value=>11}],
 ["form", {:freq=>9, :cap_freq=>1, :value=>11}],
 ["animal", {:freq=>12, :cap_freq=>0, :value=>12}],
 ["basic", {:freq=>10, :cap_freq=>1, :value=>12}],
 ["processes", {:freq=>13, :cap_freq=>0, :value=>13}]]

Я смутил это как хэш и написал следующий код для достижения моей задачи:

sorted_hash.each do |key,value|
  array.push key
end

И я действительно получил то, что хотел. Но после некоторого размышления и игры в Pry я удивляюсь, почему. Метод each Ruby Doc для массивов показывает только примеры с одной переменной item, как в

each { |item| block } → ary

но я использую две переменные, как и для хэши. Будет ли Ruby пытаться сопоставить данные элемента, которые в этом случае преуспевают, поскольку массив 2-го уровня имеет длину 2? Рекомендуется ли это делать так? Есть ли более идиоматические способы сделать это?

arrays ruby hash


2 ответа


3 Решение Cary Swoveland [2015-08-02 19:27:00]

Ответ следует из того, как в Ruby реализовано "параллельное назначение".

Как вы, наверное, знаете:

a,b,c   = 1,2,3
a #=> 1 
b #=> 2 
c #=> 3 

a,b,c   = [1,2,3]
a #=> 1 
b #=> 2 
c #=> 3 

a,b     = [1,2,3]
a #=> 1 
b #=> 2 

a,*b    = [1,2,3]
a #=> 1 
b #=> [2, 3] 

*a,b    = [1,2,3]
a #=> [1, 2] 
b #=> 3

a,(b,c) = [1,[2,3]]
a #=> 1 
b #=> 2 
c #=> 3 

a,(b,(c,d)) = [1,[2,[3,4]]]
a #=> 1 
b #=> 2 
c #=> 3 
d #=> 4 

В двух последних примерах используется "неоднозначность", которую некоторые люди предпочитают называть "декомпозицией".

Теперь посмотрим, как это относится к назначению значений для блокировки переменных.

Предположим, что:

arr = [["studies", {:freq=>11, :cap_freq=>0, :value=>11}],
       ["theory",  {:freq=>9,  :cap_freq=>1, :value=>11}]]

и выполняем:

arr.each { |a| p a }
["studies", {:freq=>11, :cap_freq=>0, :value=>11}]
["theory", {:freq=>9, :cap_freq=>1, :value=>11}]

Посмотрите на это более внимательно. Определение:

enum = arr.each
  #=> #<Enumerator: [["studies",   {:freq=>11, :cap_freq=>0, :value=>11}],
  #                  ["theory",    {:freq=>9,  :cap_freq=>1, :value=>11}]]:each> 

Первый элемент передается блоку и присваивается блочной переменной v:

v = enum.next
  #=> ["studies",  {:freq=>11, :cap_freq=>0, :value=>11}]     

Мы можем предпочесть использовать параллельное назначение с двумя блочными переменными (после enum.rewind до reset перечислителя):

a,h = enum.next
a #=> "studies" 
h #=> {:freq=>11, :cap_freq=>0, :value=>11} 

Это позволяет нам написать (например):

arr.each { |a,h| p h }
{:freq=>11, :cap_freq=>0, :value=>11}
{:freq=>9, :cap_freq=>1, :value=>11}

Здесь мы не используем блок-переменную a. Если это так, мы можем заменить его локальной переменной _ или, возможно, _a:

arr.each { |_,h| p h }
arr.each { |_a,h| p h }

Это обращает внимание на то, что a не используется и может помочь избежать ошибок. Что касается ошибок, предположим, что мы хотим:

[[1,2],[3,4]].map { |a,b| puts 1+b }
  #=> [3,5]

но непреднамеренно напишите:

[[1,2],[3,4]].map { |a,b| puts a+b }
  #=> [3,7]

который выполняет только штраф (но создает неверный результат). Напротив,

[[1,2],[3,4]].map { |_,b| puts a+b }
  #NameError: undefined local variable or method 'a'

говорит о наличии проблемы.

Вот более подробный пример того, что вы можете сделать в блоках с параллельным назначением и значениями. Дано:

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

предположим, что мы хотим получить:

    { :a=>3, :b=>7 }

Один из способов:

h.each_with_object({}) { |(a,(b,c)),g| g[a] = b+c }
  => {:a=>3, :b=>7}

2 AmitA [2015-08-02 16:40:00]

Это потому, что Ruby позволяет вам сделать это:

[[1,2,3], [4,5,6]].each {|x,y,z| puts "#{x}#{y}#{z}"}
# 123
# 456

Таким образом, each дает элемент массива блоку, и поскольку синтаксис блока Ruby позволяет "расширять" элементы массива их компонентам, предоставляя список аргументов, он работает.

Вы можете найти больше трюков с блочными аргументами здесь.

И, кстати, вместо создания массива самостоятельно и вызова push вы можете просто сделать следующее, так как map возвращает массив:

sorted_hash.map(&:first)