Kilka słów o Symbol#to_proc, returning i alias_method_chain.
4 komentarze | Kategorie: Merb, Ruby, Ruby on Rails, Techblog, Tips & tricks | trackbackTagi: procs rails returning ruby symbol
W ostatnim wpisie zatytułowanym "It is a bug if..." poruszyłem kwestię kilku konstrukcji, które przez zespół programistyczny Merba są uważane za niedozwolone (a nawet więcej - niepoprawne). Ponieważ znaleźli się chętni by poczytać o samych konstrukcjach to je opisuję. Nie podejmuję się natomiast (chociaż pierwotnie miałem to zrobić) dyskusji czy podejście programistów Merba jest uzasadnione, prawdopodobnie zrobię to kolejnym razem.
Symbol#to_proc
["a", "ab", "abc"].map {|item| item.length }
users = User.find(:all).group_by {|user| user.name }
all_posts = Post.find(:all)
deleted_posts = all_posts.find_all {|post| post.deleted? }
# to samo z użyciem Symbol#to_proc
["a", "ab", "abc"].map(&:length)
users = User.find(:all).group_by(&:name)
all_posts = Post.find(:all)
deleted_posts = all_posts.find_all(&:deleted?)
Jeśli zastanawiasz się w jaki sposób to działa to już wyjaśniam. Ruby pozwala na dołączenie do wywoływanej metody blok kodu. Ponieważ taki blok jest domknięciem to Ruby pozwala także na użycie obiektu Proc jako dołączanego bloku. By taki obiekt nie został potraktowany jako zwykły parametr należy poprzedzić go znakiem & i umieścić na końcu listy parametrów. Oto przykład:
def twice
2.times { yield }
end
twice { puts "klasyczny blok kodu" }
clousure = Proc.new { puts "domknięcie" }
twice(&clousure)
W pierwszym przykładzie użyto konstrukcji &:symbol. Pytanie tylko w jaki sposób symbol zostaje zamieniony na obiekt Proc? I tutaj wkracza mała magia. Ruby sprawdza czy podany obiekt jest obiektem Proc. Jeśli tak właśnie jest to dany obiekt zostanie użyty jako dołączony blok kodu. W przeciwnym wypadku Ruby sprawdzi czy obiekt odpowiada na wywołanie metody to_proc i jeśli tak jest to zostanie wywołana ta metoda, a zwrócony obiekt (którym powinien być obiekt Proc) użyty jako dołączony blok kodu do metody. Jeśli wydaje Ci się to zagmatwane to prześledź kolejny przykład, który pokazuje wszystkie możliwe scenariusze.
# Konstrukcja &obiekt jako ostatni parametr w wywołaniu metody
def just_yield
yield
end
def test_object(o, msg)
puts "-" * 40, msg
just_yield(&o)
puts "[OK] Działa"
rescue => e
puts "[ERROR] #{e.message}"
end
# Przypadek 1. Przekazujemy obiekt Proc.
o1 = Proc.new { puts "p1 odpalony" }
test_object(o1, "obiekt Proc")
# Przypadek 2. Przekazujemy obiekt niebędący obiektem Proc, który nie odpowiada na 'to_proc'.
o2 = Object.new
test_object(o2, "surowy obiekt Object")
# Przypadek 3. Przekazujemy obiekt niebędący obiektem Proc, który odpowiada na 'to_proc' ale zwraca obiekt inny niż Proc.
o3 = Object.new
def o3.to_proc
puts " * o3.to_proc wywołane"
return "string"
end
test_object(o3, "surowy obiekt Object z metodą to_proc zwracającą string")
# Przypadek 4. Przekazujemy obiekt niebędący obiektem Proc, który odpowiada na 'to_proc' i zwraca obiekt Proc.
o4 = Object.new
def o4.to_proc
puts " * o4.to_proc wywołane"
return Proc.new { puts "o4 został skonwertowany i wywołany" }
end
test_object(o4, "surowy obiekt Object z metodą to_proc zwracającą Proc")
Zwróć uwagę na to, że konstrukcja &obiekt zostanie zinterpretowana w ten sposób tylko przy wywoływaniu metody. Zatem nie można oczekiwać, że proc = &object spowoduje skonwertowanie na obiekt Proc.
Wracając do naszego Symbol#to_proc teraz już powinno być oczywiste jak to działa. My piszemy "fruits.sort_by(&:weight)" a Ruby widząc &:weight wywołuje na nim metodę to_proc, ta zwraca obiekt Proc, który zostaje użyty jako blok kodu... Ot cała magia dzięki kilku linijkom kodu:
class Symbol
def to_proc
Proc.new{|*args| args.shift.__send__(self, *args)}
end
end
Ach, zapomniałbym o najważniejszym. Nie znajdziesz tej metody w Ruby 1.8, dopiero Ruby 1.9 ma ją wbudowaną. Natomiast znajdziesz ją w ActiveSupport (zatem również w railsach) a także w Ruby Facets.
returning()
To bardzo prosta metoda, dzięki której zamiast
def create_book
book = Book.new
book.title = "Książka bez tytułu"
book.pages = 200
book.save
return book
end
...napiszemy...
def create_book
returning(Book.new) do |book|
book.title = "Książka bez tytułu"
book.pages = 200
book.save
end
end
Podobnie jak metoda Symbol#to_proc także i returning nie jest dostępne standardowo w Rubym. Za to jak można się domyślić metoda ta jest dostępna w railsach.
alias_method_chain
Wyobraź sobie, że istnieje jakaś klasa, a w niej jakaś metoda a Ty chciałbyś ją trochę "przerobić" by robiła to co do tej pory oraz coś dodatkowego. Z takim problemem spotkałem się gdy chciałem by metoda Array#index obsługiwała także bloki (do wersji 1.8.6 niestety tak nie jest) co opisałem we wpisie "Nieinwazyjny monkey-patching".
Z jednej strony byłem zmuszony nadpisać istniejącą metodę, z drugiej chciałem tylko dołożyć przypadek z blokiem, a przypadek bez bloku powinien był działać tak jak do tej pory. Na szczęście Ruby w dosyć prosty sposób umożliwia rozwiązanie takiego problemu poprzez użycie alias_method. Poniżej umieszczam kod realizujący to zadanie:
class Array
alias_method :__old_index__, :index
def index(obj = nil)
if block_given?
self.each_with_index do |e, i|
return i if yield(e)
end
return nil
else
return __old_index__(obj)
end
end
end
Można tutaj zauważyć pewien schemat działania. Najpierw musimy umożliwić sobie dostęp do starej wersji metody za pomocą alias_method. Następnie nadpisujemy metodę, a w jej ciele odwołujemy się (jeśli zajdzie taka potrzeba) do starej wersji już pod nową nazwą. Kod z użyciem alias_method_chain wyglądałby tak:
require "active_support"
class Array
def index_with_block_support(obj = nil)
if block_given?
self.each_with_index do |e, i|
return i if yield(e)
end
return nil
else
return index_without_block_support(obj)
end
end
alias_method_chain :index, :block_support
end
Jak ten widzisz kod trochę różni się od pierwszej wersji. Używając alias_method_chain tworzymy wpierw metodę o nazwie <stara_nazwa>_with_<nazwa_właściwości> (u nas index_with_block_support). Po zdefiniowaniu tej metody za pomocą wywołania
alias_method_chain :<stara_nazwa>, :<nazwa_właściwości>
zostaną utworzone 2 aliasy. Jeden o nazwie <stara_nazwa>_without_<nazwa_właściwości> aliasujący do <stara_nazwa>, drugi o nazwie <stara_nazwa> aliasujący do <stara_nazwa>_with_<nazwa_właściwości>. Łatwiej to chyba wytłumaczyć kodem:
alias_method_chain :<stara_nazwa>, :<nazwa_właściwości>
# jest równoważne
alias_method :<stara_nazwa>_without_<nazwa_właściwości>, :<stara_nazwa>
alias_method :<stara_nazwa>, :<stara_nazwa>_with_<nazwa_właściwości>
Gdybyś miał jeszcze problemy to podstaw stara_nazwa=index, nazwa_właściwości=block_support. Na pocieszenie powiem, że za każdym razem kiedy widzę użycie alias_method_chain to muszę dobrze się zastanowić co tak na prawdę robi kod go używający.
Zastanawia mnie: – po cholere to returning? – dlaczego to takie zle?
Symbol#to_proc chyba nie jest taki zły (sam czasem kozystam) byle by tylko nie przesadzic
Odnośnie mojego poprzedniego wpisu to myślę, że źle zostałem zrozumiany. To nie jest tak, że te metody są złe lub dobre. Panowie od Merba przyjęli pewne konwencje i starają się ich trzymać. Z tych 3 to Symbol#to_proc jest ok, ale te 2 pozostałe jak dla mnie nie istnieją. Object#tap z ruby1.9 paradoksalnie pomimo takiej samej implementacji jak Object#returning służy do czego innego. Railsowa wersja sugeruje, że chodzi o zwracaną wartość, natomiast tap służy do wstrzyknięcia się w kilka połączonych wywołań. Takie zastosowanie ma jak najbardziej sens.
Symbol#to_proc jest dużo wolniejszy od zwyczajnego bloku:
http://www.igvita.com/2008/07/08/6-optimization-tips-for-ruby-mri/
„If you’re a Rails developer, you’ve probably used Symbol.to_proc. Well, you’re likely incurring an order of magnitude speed decrease when you do! Next time, use a block”
Tutaj trochę więcej info na ten temat:
http://rails.lighthouseapp.com/projects/8994/tickets/484-remove-symbol-to_proc-from-framework-code
Ale kiedy w Rails będzie Ruby 1.9 (jakoś na razie tego nie widać...) to wszystko powinno być w porządku, bo wtedy to będzie zarówno dobre, jak i szybkie :)
Tutaj w komentarzach (stare, ale aktualne) kolejne przykłady benchmarków:
http://blog.hasmanythrough.com/2006/3/7/symbol-to-proc-shorthand