Idiomatyczny Ruby
5 komentarzy | Kategorie: Ruby, Techblog, Tips & tricks | trackbackTagi: idioms ruby tips
Na wstępie chciałbym Was powitać po dłuższej przerwie. Zleciały wakacje, zleciał wrzesień a ja nie zmusiłem się do napisania ani jednego wpisu. Wynika to z mojego podejścia do pisania. Otóż każdy wpis traktowałem dosyć poważnie i zawsze chciałem napisać coś sensownego co w praktyce oznaczało obszernego (wystarczy spojrzeć na poprzednie wpisy). Napisanie jednego artykułu trwało kilka (a nawet więcej) godzin. W najbliższej przyszłości prawdopodobnie się to zmieni i pojawiać się będą także bardzo krótkie wpisy, ale się zobaczy :). To tyle tytułem wstępu.
Świat idiomów
W dzisiejszym wpisie postaram się przybliżyć Wam temat idiomów (nie mylić z idiotami;)), ze szczególnym uwzględnieniem Rubiego rzecz jasna (niech nie zmyli Was wstęp, to nie jest rozprawka na język polski! :D). Zacznijmy od wstępu teoretycznego. Cytując za Wikipedią:
Idiom, idiomat, idiomatyzm – wyrażenie językowe (najczęściej kilkuwyrazowe) którego znaczenie jest inne niż by to wynikało ze znaczenia poszczególnych wyrazów wchodzących w skład idiomu oraz z reguł językowych.
O idiomach najczęściej wspomina się w kontekście nauki języków obcych (ponieważ stanowią one liczną grupę wyjątków od poznawanych przez uczącego się reguł), stąd też bardzo często stosuje się inną definicję:
Idiom – wyrażenie właściwe tylko danemu językowi, nie dające się dosłownie przetłumaczyć na inny język.
Przykłady polskich idiomów
- piąte koło u wozu 'osoba lub rzecz zawadzająca'
- urwanie głowy 'bezładny pośpiech'
Po co nam idiomy? Ponieważ używając ich dostajemy wiele korzyści. Możemy wyrazić w bardzo zwięzły sposób coś, czego opisanie przy użyciu "normalnych" słów zajęłoby nam dłużej ("O wilku mowa" zamiast "Ooo, cześć. Wiesz właśnie o Tobie rozmawialiśmy"). Dają nam także nowy zasób słownictwa, są zastępstwem innych wyrażeń ("Mam tyle co kot napłakał" zamiast "Mam niewiele"). A że profesor Miodek ze mnie marny, na tym poprzestanę.
Wtf?
Czy to co napisałem do tej pory ma coś wspólnego z programowaniem? Gdyby nie miało to nie pisałbym ;). Programując używamy języków programowania. A te jak inne języki rządzą się podobnymi prawami (co nie oznacza, że takimi samymi). Ponieważ idiomy to zlepek innych słów, istnieją także i w językach programowania. Możliwe, że używamy ich nie wiedząc o tym. Dla przykładu, chcąc napisać nieskończoną pętlę w języku C zazwyczaj napiszemy:
while (1) {
//kod
}
Ale możemy także napisać:
for (;;) {
//kod
}
Oba rozwiązania są równoważne. To pierwsze rozwiązanie można by określić jako typowe, to drugie jako idiomatyczne, tym bardziej, że pętla for kojarzy się z pętla o konkretnej liczbie kroków, dających się wyznaczyć jeszcze przed uruchomieniem pętli. Ciekawą rzeczą jest tu fakt, że niewprawiony programista może mieć problemy ze zrozumieniem tej drugiej konstrukcji. Nie wygląda ona jak typowa pętla for (for (x = 0; x < 10; x++)), a zlepione średniki mogą wprowadzić irytuację. Zwracam na to uwagę, gdyż znajomość języka nie oznacza automatycznej znajomości idiomów. Odczuć to można na własnej skórze ucząc się języka obcego. Ma to dokładnie takie samo znaczenie w przypadku programowania. Idiomy dają nowe możliwości, ale nic za darmo. Musimy poświęcić im troszkę czasu.
Idiomy Rubiego
Szczerze mówiąc z idiomami (jeśli chodzi o programowanie) spotkałem się dopiero w Rubym. Wydaje mi się, że jest to spowodowane tym iż język ten zwiera takie elementy, które są niespotykane (albo nie są tak bardzo popularne). Do tej pory trochę przynudziłem (nie śpijcie jeszcze;)), teraz przejdę do konkretnych przykładów, porównując różne podejścia do tego samego problemu (pojawią się też inne języki niż Ruby).
Chcąc zadeklarować prostą klasę, z jednym atrybutem moglibyśmy napisać tak (wzorując się np. na Javie, C++):
class SomeClass
def attr
@attr
end
def attr=(attr)
@attr = attr
end
end
Ale oczywiście lepiej jest zrobić tak (chodź w tym wypadku zahaczamy także o tematykę DSL, ale o tym innym razem):
class SomeClass
attr_accessor :attr
end
Chcąc napisać funkcję (metodę) która przyjmuje dowolną liczbę argumentów w języku C moglibyśmy napisać tak:
#include <stdarg.h>
#include <stdio.h>
void print_nums(int count, ...)
{
va_list ap;
int i;
va_start(ap, count); /* Initialize the argument list. */
for (i = 0; i < count; i++)
{
int num = va_arg(ap, int); /* Get the next argument value. */
printf("%d\n", num);
}
va_end(ap); /* Clean up. */
}
main()
{
print_nums(3, 102, 5, 689);
}
W Rubym znacznie prościej:
def print_nums(*numbers)
numbers.each {|n| puts n }
end
print_nums(1, 2, 3, 4)
Chcemy zwrócić lub przypisać nową wartość do zmiennej w zależności od tego czy inna zmienna ma wartość obliczana jest jako prawda lub fałsz.
if myvar
return myvar
else
return another_value
end
Lepiej jest jednak napisać:
return myvar || another_value
Chcemy przypisać nową wartość zmiennej, jeśli jej wartość obliczana jest jako fałsz.
if not breakfast
breakfast = :bacon
end
#albo
breakfast = :bacon unless breakfast
Jednak wprawny programista Rubiego napisze:
breakfast ||= :bacon
Ten sposób jest bardzo często wykorzystywane w metodach zwracających jakąś wartość, która musi wpierw być obliczona.
def current_user
return @current_user ||= User.find(session[:id]) #uwaga na wyjątek jeśli session[:id] zwróci nil
end
Chcemy do pliku dołączyć pewną część kodu, która zostanie uruchomiona tylko wtedy gdy plik został wywołany bezpośrednio (ruby plik.rb) a nie poprzez require czy load. (W ten sposób można dołączyć np. testy bezpośrednio w kodzie.) Użyjemy zmiennej $0 lub jej aliasu $PROGRAM_NAME.
if $PROGRAM_NAME == __FILE__ #
do_stuff
end
Niesamowicie idiomatyczne są bloki kodu w Rubym. Oto kilka przykładów. Bardzo często wykonując pewne operacje pracujemy na pewnych zasobach. Zasobem może być w zasadzie cokolwiek. Najbardziej typowym przykładem jest baza, plik, gniazdo (socket). Zasób taki wpierw pozyskujemy, następnie wykonujemy na nim jakieś operacje (zapis danych, odczyt) i oddajemy (dzięki czemu system odzyskuje z powrotem zasoby. Wydawać by się mogło, że nie ma tu nic czego mielibyśmy się obawiać. To na co musimy zawsze zwrócić uwagę to ten 3 (najważniejszy) krok - zwolnienie zasobu. Nawet jeśli w kodzie umieścimy odpowiednie operacje zwalniające, nie mamy gwarancji, że tak się stanie (np. rzucony i nieobsłużony wyjątek już po otwarciu pliku, a przed jego zamknięciem). I tu właśnie należy wręcz skorzystać z następującego rozwiązania:
class Resource
def self.open(identifier)
resource = Resource.new(identifier)
yield resource
ensure
resource.close
end
def close
#...
end
end
Resource.open(identifier) do |resource|
process(resource)
end
# zasób jest zamknięty, nawet jeśli pojawił się wyjątek
Wygląda znajomo? Powinno :). Takie rozwiązanie zastosowano w klasie File.
File.open("plik.txt", "w") do |file|
#...
end
# nie martwimy się o zamknięcie pliku
Jeśli chcemy obsłużyć zarówno klasycznie otwarcie zasobu (bierzemy odpowiedzialność za jego zamknięcie) jak i to powyższe napiszemy coś takiego:
def Resource.open(identifier)
resource = Resource.new( identifier )
if block_given?
begin
yield resource
ensure
resource.close
end
else
return resource
end
end
r = Resource.open(x)
#...
r.close
#lub
Resource.open(x) do |r|
#...
end
Bloki (nawet jeśli chodzi o sam wygląd w kodzie) bardzo ładnie reprezentują tranzakcje. Spójrz proszę na kod:
Account.transaction(david, mary) do
david.withdrawal(100)
mary.deposit(100)
end
Jeśli podczas wywołania tego bloku wystąpi wyjątek, tranzakcja zostanie odwołana. To jest o tyle piękne, że w tradycyjnym podejściu postępowalibyśmy tak: otworzyć trazakcję, wykonać operacje, zamknąć tranzakcję. W tym wypadku nie dość, że omijamy pierwszy i ostatni krok (automatycznie) to jeszcze sam blok w pięknie obrazuje nam znaczenie tranzakcji (albo cały blok, albo nic).
Następny przykład. Chcemy z dwóch liczb wybrać większą (bądź mniejszą) i zwrócić ją. Klasycznie zrobimy tak:
def maximum(a, b)
if a > b
return a
else
return b
end
end
Jednakże idiomatycznie będzie tak (a także krócej, czytelniej!):
def maximum(a, b)
return [a, b].max # czy to w ogóle wymaga komentarza?
end
Takich przykładów można podawać mnóstwo. Na koniec podam przykład idiomu, na który natknąłem się dosyć niedawno, a który bardzo mi się spodobał. Chodzi o sytuację, kiedy chcemy wywołać jakąś metodę i zapamiętać zwróconą wartość. Jeśli wywołanie nie powiedzie się (wyjątek) chcemy wywołać jakąś inną metodę (może być wiele takich metod). Klasycznie zrobilibyśmy to tak:
value = nil
begin
value = do_something1
rescue => e
begin
value = do_something2
rescue => e
#...
end
end
Ale po co tak robić jak można tak:
value = do_something1 rescue do_something2 rescue "sorry"
Idiomów jest znacznie więcej i nie jestem w stanie ich tu podać wszystkich :). Ważne jest to, żeby znać te najczęściej stosowane, gdyż ułatwia nam to wspólne porozumiewanie się. Na koniec podaję dodatkowe źródła, do których przeglądnięcia bardzo zachęcam :). Czekam też na Wasze ulubione idomy, niekoniecznie te Rubinowe.
Zasoby
http://www.rubygarden.org/ruby/page/show/RubyIdioms
http://renaud.waldura.com/doc/ruby/idioms.shtml
http://podcast.sdruby.com/2007/1/16/episode-014-ruby-idioms-part-1
Przydatny wpis, ciekawe przykłady :). Jesteś w moim czytniku.
Przykładów jest znacznie więcej – zaglądnij do linków, które podałem na końcu :).
Ciekawy post. Good job !
Naprawdę przydatny artykuł. Piszę pracę licencjacką o Ruby i często zaglądam na Twoją stronę. Tak trzymać:)
@kotosha, dzięki za słowa uznania :). Ruby na tym blogu króluje i nie zamierzam tego zmieniać.