Nowości i zmiany w Ruby 1.9 #3 - zmiany odnośnie argumentów metod
2 komentarze | Kategorie: Ruby, Techblog | trackbackTagi: 1.9 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 mały cykl tym razem chciałbym opisać zmianę dotyczącą parametrów metod. Do tej pory deklaracja metody wyglądała następująco: def fun(r1, r2, .., rm, o1=v1, o2=v2, ..., on=vn[, *rest][, &block]).
Suchy zapis jest często ciężki w odbiorze, zatem opisując słownie mamy: lista parametrów wymaganych (może być pusta), lista parametrów opcjonalnych (może być pusta), opcjonalny argument zbierający pozostałe parametry, opcjonalny parametr bloku. Prezentuje to przykładowy kod:
def foo(a, b = 1, *rest, &block)
p [a, b, rest, block]
end
foo(10)
foo(10, 20)
foo(10, 20, 30)
Uruchamiając powyższy program otrzymamy:
[10, 1, [], nil]
[10, 20, [], nil]
[10, 20, [30], nil]
Parametry wymagane z prawej strony listy?
Nowością, jakby nie patrzeć dosyć kontrowersyjną, jest możliwość umieszczenia wymaganych parametrów po parametrach opcjonalnych oraz *rest, ale przed &block.
Zatem formalnie zapisując wygląda to tak: def fun([r1, r2, .., rm][, o1=v1, o2=v2, ..., on=vn][, *rest][, rm+1, rm+2, ..., rm+l][, &block]).
Opisując słownie mamy: lista parametrów wymaganych (może być pusta), lista parametrów opcjonalnych (może być pusta), opcjonalny argument zbierający pozostałe parametry, lista parametrów wymaganych (może być pusta), opcjonalny parametr bloku. Prezentuje to przykładowy kod:
def foo(a, b = 1, *rest, c, &block)
p [a, b, rest, c, block]
end
foo(10, 20)
foo(10, 20, 30)
foo(10, 20, 30, 40)
Powyższy przykład tworzy na wyjściu:
[10, 1, [], 20, nil]
[10, 20, [], 30, nil]
[10, 20, [30], 40, nil]
Na początku wydaje się bardzo ciężkie do rozszyfrowania, który parametr pójdzie do którego argumentu, ale po kilku chwilach okazuje się, że nie jest to takie trudne. Należy zwrócić uwagę na kilka rzeczy:
1) musimy przekazać przynajmniej tyle parametrów ile jest wymaganych (czyli m+l)
2) jeśli nie użyto argumentu przechwytującego resztę parametrów (*rest) to można przekazać
maksymalnie n+m+l parametrów
3) nie można przeplatać argumentów opcjonalnych z wymaganymi
4) argumenty wymagane r1..rm są dopasowywane od lewej strony parametrów wywołania
5) argumenty wymagane rm+1..rm+l są dopasowywane od prawej strony parametrów wywołania
6) pozostałe argumenty są dopasowywane do argumentów opcjonalnych i argumentu przechwytującego (*rest)
Zwróć uwagę, że punkty 1, 2, 3, 4, 6 obowiązywały do tej pory. Zatem tak na prawdę doszła tylko jedna zasada.
WTF? OMFG! LOL!
Domyślam się co chcesz powiedzieć. Zapewne coś w stylu "WTF, o co tu chodzi, po co, dlaczego? Nie widzę w tym w ogóle sensu.". Ja też podobnie na to zareagowałem (do tej pory nie spotkałem się z czymś takim). Zamiast jednak rozpaczać możemy podejść do tego na dwa sposoby (my jako społeczność). Po pierwsze można to zupełnie zignorować, udawać, że tego nie było i nie ma, nie używać i namawiać innych żeby tego nie robili. Możemy też siąść i spróbować opracować rzeczywiste zastosowania. W końcu nie raz z dziwnych pomysłów powstawały ciekawe rzeczy. A przecież Ruby posiada wiele różnych, w pewnym sensie "egzotycznych", konstrukcji, które czekają na specjalną okazję by je zastosować. W końcu ile razy użyłeś w kodzie Object#instance_eval, Module#const_missing, Module#nesting, Proc#binding? Prawdopodobnie niewiele, a może nawet w ogóle. Dlatego nie rozpaczajmy, tylko pomyślmy do czego możemy wykorzystać tą nową konstrukcję.
Koichi SASADA pokazał ostatnio na ruby-core jedno z zastosowań, które dotyczy metody []=.
class MyArray < Array
def []=(*args, value)
p [args, value]
end
end
arr = MyArray.new
arr[1, 2, 3] = "foo"
arr[100] = "bar"
Program wypisuje:
[[1, 2, 3], "foo"]
[[100], "bar"]
W przypadku metod <nazwa>= jako ostatni parametr (który i tak jest wymagany) dostajemy wartość po prawej stroni przypisania. Jak widzisz możliwość umieszczenia wymaganego parametru po prawej stronie już była wcześniej w języku ;-).
Myślę, że jednym z zastosowań może być użycie pseudo "nazwanych argumentów", czyli hasha z opcjami.
# stary sposób
def before_filter(*actions)
options = actions.last.is_a?(Hash) ? actions.pop : {}
# ...
end
before_filter :authenticate, :authorize, :except => "list"
before_filter :authenticate, :authorize
# nowy sposób
def before_filter(*actions, options)
# ...
end
before_filter :authenticate, :authorize, except: "list"
before_filter :authenticate, :authorize, {}
Niestety nowy sposób też ma swoją wadę, bo trzeba podać jawnie pusty hash w przypadku braku dodatkowych opcji (chociaż dla niektórych to może być zaleta).
Z parametrami metod wiąże się jeszcze jedna zmiana. Otóż argumenty można grupować za pomocą (), co spowoduje użycie semantyki przypisania równoległego do podanych parametrów. Pokazuje to ostatni już przykład.
def foo(a, (b, c))
p [a, b, c]
end
foo(1, 2) # a = 1, b = 2, c = nil
foo(1, []) # a = 1, b = nil, c = nil
foo(1, [2, 3]) # a = 1, b = 2, c = 3
foo(1, [2, 3, 4]) # a = 1, b = 2, c = 3
def bar((head1, *rest1), (head2, *rest2))
p [head1, rest1, head2, rest2]
end
bar([], []) # head1 = nil, rest1 = [], head2 = nil, rest2 = []
bar([1], 2) # head1 = 1, rest1 = [], head2 = 2, rest2 = []
bar([1, 2, 3], [4, 5, 6]) # head1 = 1, rest1 = [2, 3], head2 = 4, rest2 = [5, 6]
Składnia before_filter :authenticate, :authorize, except: „list” możliwa jest też bez żadnego parametru wymaganego na końcu. I na dodatek nie trzeba podawać {} jak nic nie ma
Mętna i nieintuicyjna jest ta nowa składnia z wymaganymi parametrami za opcjonalnymi. Glupi pomysl. Przyklad Koichi jest na sile, nie widac zadnej wymiernej korzysci z takiej skladni.
Tak, możliwe że ten przykład trochę naciągany. Myślę, że to po prostu nie będzie używane w praktyce przez programistów (chyba, że przez szaleńców).