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?
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.
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
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}}
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
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)) }
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ć
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