Ruby (Rails) # inject on hashes-dobry styl ?

Wewnątrz kodu Rails ludzie używają metody Enumerable#inject do tworzenia hashów, jak to:

somme_enum.inject({}) do |hash, element|
  hash[element.foo] = element.bar
  hash
 end

Choć wydaje się, że stało się to powszechnym idiomem, czy ktoś widzi przewagę nad" naiwną " wersją, która brzmiałaby tak:

hash = {}
some_enum.each { |element| hash[element.foo] = element.bar }

Jedyną zaletą, jaką widzę dla pierwszej wersji jest to, że robisz to w zamkniętym bloku i nie inicjalizujesz (jawnie) hasha. W przeciwnym razie nadużywa metody w nieoczekiwany sposób, jest trudniejszy do zrozumienia i trudniejszy do odczytania. Dlaczego jest tak popularny?

Author: James Webster, 2010-07-12

6 answers

Piękno jest w oku patrzącego. Ci, którzy mają jakieś podstawy programowania funkcyjnego, prawdopodobnie wolą metodę opartą na inject (tak jak ja), ponieważ ma ona taką samą semantykę jak fold funkcja wyższego rzędu , która jest powszechnym sposobem obliczania pojedynczego wyniku z wielu wejść. Jeśli rozumiesz inject, powinieneś zrozumieć, że funkcja jest używana zgodnie z przeznaczeniem.

Jako jeden z powodów, dla których takie podejście wydaje się lepsze (moim oczom), rozważ zakres leksykalny zmiennej hash. W metodzie inject, hash istnieje tylko w ciele bloku. W metodzie each zmienna hash wewnątrz bloku musi zgadzać się z pewnym kontekstem wykonania zdefiniowanym poza blokiem. Chcesz zdefiniować inny hash w tej samej funkcji? Korzystając z metody inject, można wyciąć i wkleić kod oparty na inject i użyć go bezpośrednio, a to prawie na pewno nie wprowadzi błędów (ignorując, czy należy używać C & P podczas edycji - osób). Używając metody each, musisz C & P kodu I zmienić nazwę zmiennej hash na dowolną nazwę, której chcesz użyć - dodatkowy krok oznacza, że jest to bardziej podatne na błędy.

 22
Author: Aidan Cully,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-07-12 18:12:32

Jak wskazuje Aleksey, Hash#update() jest wolniejszy niż Hash#store (), ale to dało mi do myślenia o ogólnej wydajności # inject () vs prostej # każdej pętli. Tak więc porównałem kilka rzeczy:

(uwaga: Zaktualizowano 19 września 2012 r., aby dołączyć #each_with_object)

(uwaga: Zaktualizowano 31 marca 2014 r. o dodanie #by_initialization, dzięki sugestii https://stackoverflow.com/users/244969/pablo )

Testy

require 'benchmark'
module HashInject
  extend self

  PAIRS = 1000.times.map {|i| [sprintf("s%05d",i).to_sym, i]}

  def inject_store
    PAIRS.inject({}) {|hash, sym, val| hash[sym] = val ; hash }
  end

  def inject_update
    PAIRS.inject({}) {|hash, sym, val| hash.update(val => hash) }
  end

  def each_store
    hash = {}
    PAIRS.each {|sym, val| hash[sym] = val }
    hash
  end

  def each_update
    hash = {}
    PAIRS.each {|sym, val| hash.update(val => hash) }
    hash
  end

  def each_with_object_store
    PAIRS.each_with_object({}) {|pair, hash| hash[pair[0]] = pair[1]}
  end

  def each_with_object_update
    PAIRS.each_with_object({}) {|pair, hash| hash.update(pair[0] => pair[1])}
  end

  def by_initialization
    Hash[PAIRS]
  end

  def tap_store
    {}.tap {|hash| PAIRS.each {|sym, val| hash[sym] = val}}
  end

  def tap_update
    {}.tap {|hash| PAIRS.each {|sym, val| hash.update(sym => val)}}
  end

  N = 10000

  Benchmark.bmbm do |x|
    x.report("inject_store") { N.times { inject_store }}
    x.report("inject_update") { N.times { inject_update }}
    x.report("each_store") { N.times {each_store }}
    x.report("each_update") { N.times {each_update }}
    x.report("each_with_object_store") { N.times {each_with_object_store }}
    x.report("each_with_object_update") { N.times {each_with_object_update }}
    x.report("by_initialization") { N.times {by_initialization}}
    x.report("tap_store") { N.times {tap_store }}
    x.report("tap_update") { N.times {tap_update }}
  end

end

The wyniki

Rehearsal -----------------------------------------------------------
inject_store             10.510000   0.120000  10.630000 ( 10.659169)
inject_update             8.490000   0.190000   8.680000 (  8.696176)
each_store                4.290000   0.110000   4.400000 (  4.414936)
each_update              12.800000   0.340000  13.140000 ( 13.188187)
each_with_object_store    5.250000   0.110000   5.360000 (  5.369417)
each_with_object_update  13.770000   0.340000  14.110000 ( 14.166009)
by_initialization         3.040000   0.110000   3.150000 (  3.166201)
tap_store                 4.470000   0.110000   4.580000 (  4.594880)
tap_update               12.750000   0.340000  13.090000 ( 13.114379)
------------------------------------------------- total: 77.140000sec

                              user     system      total        real
inject_store             10.540000   0.110000  10.650000 ( 10.674739)
inject_update             8.620000   0.190000   8.810000 (  8.826045)
each_store                4.610000   0.110000   4.720000 (  4.732155)
each_update              12.630000   0.330000  12.960000 ( 13.016104)
each_with_object_store    5.220000   0.110000   5.330000 (  5.338678)
each_with_object_update  13.730000   0.340000  14.070000 ( 14.102297)
by_initialization         3.010000   0.100000   3.110000 (  3.123804)
tap_store                 4.430000   0.110000   4.540000 (  4.552919)
tap_update               12.850000   0.330000  13.180000 ( 13.217637)
=> true

Podsumowanie

Enumerable # each jest szybszy niż Enumerable#inject, a Hash#store jest szybszy niż Hash#update. Ale najszybsze ze wszystkich jest przekazanie tablicy w czasie inicjalizacji:

Hash[PAIRS]

Jeśli dodajesz elementy Po utworzeniu hasha, zwycięska wersja jest dokładnie tym, co sugerował OP:

hash = {}
PAIRS.each {|sym, val| hash[sym] = val }
hash

Ale w takim przypadku, jeśli jesteś purystą, który chce pojedynczej formy leksykalnej, możesz użyć #tap i # each i uzyskać to samo prędkość:

{}.tap {|hash| PAIRS.each {|sym, val| hash[sym] = val}}

dla tych, którzy nie są zaznajomieni z tap, tworzy powiązanie odbiornika (nowy hash) wewnątrz korpusu, a na koniec zwraca odbiornik (ten sam hash). Jeśli znasz Lisp, pomyśl o nim jak o wersji LET binding Ruby.

-whew-. Dzięki za wysłuchanie.

Postscript

Ponieważ ludzie pytali, oto środowisko testowe:

# Ruby version    ruby 2.0.0p247 (2013-06-27) [x86_64-darwin12.4.0]
# OS              Mac OS X 10.9.2
# Processor/RAM   2.6GHz Intel Core i7 / 8GB 1067 MHz DDR3
 30
Author: fearless_fool,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:19

inject (aka reduce) zajmuje długie i szanowane miejsce w funkcyjnych językach programowania. Jeśli jesteś gotowy na zanurzenie i chcesz zrozumieć wiele inspiracji Matza dla Rubiego, powinieneś przeczytać przełomową strukturę i interpretację programów komputerowych , dostępną online pod adresem http://mitpress.mit.edu/sicp/.

Niektórzy programiści uważają, że stylistycznie czystsze jest posiadanie wszystkiego w jednym pakiecie leksykalnym. W przykładzie hash użycie inject oznacza: nie trzeba tworzyć pustego hasha w osobnej instrukcji. Co więcej, polecenie inject zwraca wynik bezpośrednio - nie musisz pamiętać, że znajduje się on w zmiennej hash. Aby to było naprawdę jasne, rozważ:

[1, 2, 3, 5, 8].inject(:+)

Vs

total = 0
[1, 2, 3, 5, 8].each {|x| total += x}

Pierwsza wersja Zwraca sumę. Druga wersja przechowuje sumę w total i jako programista musisz pamiętać, aby użyć total zamiast wartości zwracanej przez instrukcję .each.

Jeden mały dodatek (i czysto idomatic -- not about inject): twój przykład może być lepiej napisany:

some_enum.inject({}) {|hash, element| hash.update(element.foo => element.bar) }

...ponieważ hash.update() zwraca sam hash, nie potrzebujesz dodatkowej instrukcji hash na końcu.

Update

@ Aleksey zawstydził mnie porównaniem różnych kombinacji. Zobacz moją odpowiedź benchmarking gdzie indziej tutaj. Krótka forma:
hash = {}
some_enum.each {|x| hash[x.foo] = x.bar}
hash 

Jest najszybszy, ale może być nieco bardziej elegancki -- i jest tak samo szybki -- jak:

{}.tap {|hash| some_enum.each {|x| hash[x.foo] = x.bar}}
 10
Author: fearless_fool,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-12 17:01:17

Właśnie znalazłem w Ruby inject with initial being a hash sugestia użycia each_with_object zamiast inject:

hash = some_enum.each_with_object({}) do |element, h|
  h[element.foo] = element.bar
end
Wydaje mi się naturalne.

Inny sposób, używając tap:

hash = {}.tap do |h|
  some_enum.each do |element|
    h[element.foo] = element.bar
  end
end
 2
Author: Alexey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:32:26

Jeśli zwracasz hash, użycie merge może utrzymać go w czystości, więc nie musisz zwracać go później.

some_enum.inject({}){|h,e| h.merge(e.foo => e.bar) }

Jeśli Twoje enum jest skrótem, możesz uzyskać klucz i wartość ładnie za pomocą (k, v).

some_hash.inject({}){|h,(k,v)| h.merge(k => do_something(v)) }
 2
Author: Rodel30,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-14 15:53:58

Myślę, że ma to związek z ludźmi nie do końca rozumiejącymi, kiedy stosować reduce. Zgadzam się z Tobą, każdy jest taki jaki powinien być

 1
Author: Matt Briggs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-07-12 18:03:39