Nowości i zmiany w Ruby 1.9 #5 - bloki, domknięcia, nowa lambda
10 komentarzy | Kategorie: Ruby, Techblog | trackbackTagi: 1.9 lambda proc ruby ruby1.9
Wpis ten jest jedną z części cyklu pt "Nowości i zmiany w Ruby 1.9". Pełną listę wpisów znajdziesz pod adresem http://radarek.jogger.pl/2008/11/30/nowosci-i-zmiany-w-ruby-1-9/.
Kontynuując serię wpisów o nowościach w ruby 1.9 tym razem opiszę zmiany dotyczące bloków, domknięć oraz nowej składni lambdy.
Zacznijmy od nowej składni dla lamby, za pomocą operatora ->. Przykład pokazuje tworzenie dwóch identycznych domknięć za pomocą starego sposobu i nowego.
mul2_first = lambda {|x| x * 2 }
mul2_second = ->(x) { x * 2 }
# można także ominąć () ale w ten sposób raczej pogarszamy czytelność
mul2_third = -> x { x * 2 }
Jak widać nowa składnia pozwala na bardziej zwięzłe tworzenie domknięć, chociaż można także popsuć czytelność, np. tak:
foo(:mult => -> x, y = 2 { x * y })
# z nową składnią Hashy i nawiasami dużo lepiej
foo(mult: ->(x, y = 2) { x * y })
Zatem po raz kolejny trzeba podkreślić, że to od Ciebie zależy jak bardzo czytelny będzie kod.
Parametry domyślne i &block - a jednak da się
Każdy kto używał domknięć prędzej czy później napotykał na ich ograniczenia w Ruby. Nie można było używać domyślnych wartości, a także przyjmować bloków kodu. Było to czasem irytujące, bo zmuszeni byliśmy czasem pisać tak:
named_scope :recent,
lambda {|*args| {:conditions => ["released_at > ?", (args.first || 2.weeks.ago)]} }
Zamiast:
named_scope :recent,
lambda {|date = 2.weeks.ago| {:conditions => ["released_at > ?", date]} }
# przykład z nową składnią lambdy i hasha
named_scope :recent,
->(date = 2.weeks.ago){ {conditions: ["released_at > ?", date]} }
Ta druga forma jest już na szczęście możliwa i dotyczy to zarówno bloków jak i domknięć. Podobnie jest z przekazywaniem bloku.
def foo(&block)
puts "foo called"
block.call("from_foo") { puts "block from foo called" }
end
foo do |msg, &block|
puts "got msg: #{msg}"
puts "yielding"
block.call
end
Argumenty bloków są lokalne
Parametry bloków są teraz lokalne w stosunku do niego. Oznacza to tyle, że jeśli poza blokiem istnieje zmienna o takiej samej nazwie to jest to całkowicie inna zmienna. Najłatwiej to zabserwować różnicę w zachowaniu poniższego programu w ruby 1.8 i 1.9.
i = "foo"
puts "Przed blokiem: i = #{i}"
3.times do |i|
puts "i = #{i}"
end
puts "Poza blokiem: i = #{i}"
Oto co wypisze program uruchomiony z wersją 1.8:
Przed blokiem: i = foo i = 0 i = 1 i = 2 Poza blokiem: i = 2
Wersja 1.9 natomiast spowoduje wypisanie:
Przed blokiem: i = foo i = 0 i = 1 i = 2 Poza blokiem: i = foo
Parametr bloków jak parametry metod
Być może nie wiedziałeś, ale do tej pory można było (zwłaszcza jak ktoś lubi pisać "dziwny" kod) pisać tak:
h = {}
a = [1, 2, 3]
a.each_with_index do |@a, h["a"]|
p [@a, h["a"]]
end
Działa to dlatego, że działa tutaj semantyka przypisania, tj. kolejne przypisanie są realizowane poprzez @a = val1, h["a"] = val2 itp.
Od wersji 1.9 parametry bloków mają taką samą semantykę jak parametry metod, zatem można tylko używać "normalnych" parametrów (zmienne). Możesz nawet umieścić parametry po parametrach opcjonalnych... Dobra, nie było tematu :).
proc == Proc.new
W końcu proc jest aliasem do Proc.new (do tej pory był aliasem do ... lambda). Została także dodana metoda Proc#lambda?, która sprawdza czy obiekt domknięcia ma semantykę proc czy lambda.
closure1 = proc { }
puts closure1.lambda? #=> false
closure2 = Proc.new { }
puts closure2.lambda? #=> false
closure3 = lambda { }
puts closure3.lambda? #=> true
closure4 = ->{}
puts closure4.lambda? #=> true
def make_closure(&block); return block; end
closure5 = make_closure {}
puts closure5.lambda? #=> false
.() - nowy sposób na wywołanie domknięcia
Dodano nowy sposób na wywołanie domknięcia, tj. za pomocą closure.(). Poniższy przykład pokazuje stare sposoby oraz nowy.
closure = lambda {|s| puts "#{s}" }
closure.call("foo")
closure["bar"] # nie polecam, wygląda jak tablica...
# nowy sposób
closure.("baz") # zostanie wywołany .call
Zmienne lokalne dla domknięć
Nowością w 1.9 jest możliwość wyspecyfikowania parametrów bloków/domknięć, które mają być lokalne. Chodzi zwłaszcza o sytuację, kiedy na "zewnątrz" bloku istnieje już zmienna o takiej samej nazwie, a my nie chcemy jej użyć (za to chcemy z jakiegoś dziwnego powodu użyć takiej samej nazwy). Robi się to poprzez wylistowanie zmiennych po średniku na liście parametrów (jednak należy podkreślić, że nie jest to parametr, a zmienna lokalna bloku). Mam wrażenie, że jest to trochę zbędne...
d = 2
a = lambda{|;d| d = 1}
a.call()
puts d # => 2
Myślę, że w ten sposób opisałem wszystkie najważniejsze zmiany dotyczące bloków i domknięć.
A czy przypadkiem bloki nie są domknięciami?
> [...] lambda{|;d| d = 1}
> [...] Mam wrażenie, że jest to trochę zbędne…
niektórzy tego w lispie używają... (lambda (&aux d) ...) tudzież (lambda (&aux (d 1)) ...), podejrzewam że Matz zastosował swoją starą zasadę „portowania” lispowch ficzerów ;)
btw, można w Rubym przypisać wartość d w || ?
o, nawet udało mi się to znaleźć (wywiad z Matzem z 2003 roku), http://www.artima.com/intv/closures2.html
„Yukihiro Matsumoto: Actually, to tell the truth, the first reason is to respect the history of Lisp. Lisp provided real closures, and I wanted to follow that.”
;>
himn1 blok możesz skonwertować do domknięcia np tak:
Dopóki nie masz referencji do obiektu ciężko mówić o domknięciu. Przynajmniej w większości tekstów jest napisane „bloki i domknięcia”.
szymon niestety nie można nadawać wartości dla takich zmiennych, tzn
nie zadziała.
(Komentarz zmodyfikowany 14.12.2008 o 11:11)
No, lambda obecnie staje sie coraz bardziej popularna w wyzszej warstwie API rails/merb’a
Named scope jest tutaj dobrym przykladem. W 1.8 mam named_scope ktory reaguje na :default jako domyslnie skonfigurowany include, :all wszystkie relacje i :relacja1, :relacja2. Opisanie tego poprzez 1 argument |*relations_for_include| jakie jest kazdy widzi :P
Przy okazji blokow, zucilem proposal.
http://www.ruby-forum.com/topic/173364#new
Rozwiazalo by to problem dziedziczenia lambd w STI. Obiekty proc w inheritable attribute sa po prostu kopiowane. self pozostaje taki sam pomimo ze tak narpade obiekt sie juz zmienil. Gdyby istniala mozliwosc przedefiniowania self, bylo by wyjebiscie. VM sam w sobie pozwala na takie cos, proc_invoke zaweira oddzielnie blok i self, wiec pewnie wystarczylo by dodac nowa metoda ktora tworzy nowy blok z nowym kontekstem self, a w przypadku call_with_binding upewnic sie ze przy wielowatkowosci nic sie nie wali. Szkoda ze jestem kiepski w c :(
http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1570-named_scope-not-preserving-proper-model-in-sti
Tu jest przyklad.
BTW, nie wiem czy nie pomieszalem binding z kontekstem, moze powinno byc Proc#call_with_context i Proc#context ? :P binding to pewnie cos innego. Jesli uwazasz ze to ma sens :P pchiiiij ;-)
Paweł, do podmiany self służy instance_eval
Odpalając otrzymamy:
Wiem, ale nie dziala ;) w tamtym przypadku to raz, a dwa jak przekazac takiej lambdzie argumenty ? Sorry ale instance_eval to tylko jakis dirty hax w tym przypadku imho.
W 1.9 jest jeszcze instance_exec, do którego możesz przekazać parametry.
„Sorry ale instance_eval to tylko jakis dirty hax w tym przypadku imho.”
Z tego co piszesz chodzi Ci o podmianę „self” co też właśnie powyższe metody robią. Nie widzę tu hacka :).
wejdz na irc’a :P