Any fool can make things bigger, more complex, and more violent. It takes a touch of genius and a lot of courage to move in the opposite direction. Albert Einstein

o Ruby

Kilka słów o Symbol#to_proc, returning i alias_method_chain.

4 komentarze | Kategorie: Merb, Ruby, Ruby on Rails, Techblog, Tips & tricks | trackback
Tagi:

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.

Jeśli spodobał Ci się wpis to może umieścisz ten blog w swoim czytniku RSS?

Komentarze

1. avatar icon teamon napisał(a) 16 Paź 2008 o godz. 23:54:

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

2. avatar icon Radarek napisał(a) 17 Paź 2008 o godz. 00:02:

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.

3. avatar icon Sabon napisał(a) 18 Paź 2008 o godz. 18:14:

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 :)

4. avatar icon Sabon napisał(a) 18 Paź 2008 o godz. 18:22:

Tutaj w komentarzach (stare, ale aktualne) kolejne przykłady benchmarków:
http://blog.hasmanythrough.com/2006/3/7/symbol-to-proc-shorthand

Dodaj coś od siebie

Możesz korzystać ze składni Textile.

Pola oznaczone * są wymagane.

Proszę o dodawanie komentarzy związanych z tematem postu, sprawy osobiste proszę załatwiać przez maila bądź gg.

Zastrzegam sobie prawo do moderacji komentarzy (edycja, usuwanie).