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

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

["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 для массивов показывает только примеры с одной переменной элемента, как в

each { |item| block } → ary

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


person Flip    schedule 02.08.2015    source источник
comment
Интересно, что Hash#each также дает только один элемент (это контракт each, в конце концов, он всегда дает ровно один элемент), массив с двумя элементами, ровно как в вашем случае. Тем не менее каким-то образом, несмотря на то, что эти два случая идентичны, вас смущает один, а не другой.   -  person Jörg W Mittag    schedule 02.08.2015
comment
Должен признаться, что не понимаю, что вы хотите сказать. hash#each, похоже, ожидает/выдает два элемента: ключ и значение (each {| key, value | block } → hsh). Это не идентично each { |item| block } → ary, который, кажется, дает только один элемент. Конечно, отсутствие у меня опыта и знаний как разработчика может быть причиной непонимания вашей точки зрения.   -  person Flip    schedule 03.08.2015


Ответы (2)


Ответ следует из того, как реализовано «параллельное присваивание» в 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 для сброса перечислителя):

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}
person Cary Swoveland    schedule 02.08.2015
comment
Спасибо за этот обширный ответ, Кэри. Очень поучительно. - person Flip; 04.08.2015

Это потому, что 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)
person AmitA    schedule 02.08.2015
comment
Большое спасибо, АмитА. Ответы на все вопросы. - person Flip; 02.08.2015