DRY
maj 16th, 2006 at 11:15 pm (scheme, emacs, c++)
Termin DRY (Don’t repeat yourself) okre??la filozofiÄ? programowania, kt??ra jest przeciwno??ciÄ? popularnej poniekÄ?d metody kopiuj i wklej. StosujÄ?c jÄ? nale??y wy??apywaÄ? wszystkie schematy, jakie pojawiajÄ? siÄ? w pisanym przez nas kodzie i uog??lniaÄ? je. Najpopularniejszym na to sposobem jest u??ywanie funkcji i klas. Lisp posiada dodatkowo mo??liwo??Ä? definiowania makr, kt??re pozwalajÄ? uog??lniaÄ? kod na poziomie sk??adni (w przeciwno??ci do poziomu wykonywania programu, gdzie dzia??ajÄ? funkcje i klasy). O tym, jak bardzo jÄ?zyk Scheme sprzyja stosowaniu DRY przekona??em siÄ? podczas testowania emacsowego modu??u msf-abbrev. Jego dzia??anie polega na dope??nianiu okre??lonych przez nas nazw przez inne, zazwyczaj d??u??sze wyra??enia. DziÄ?ki przystÄ?pnemu interface’owi pisanie kodu momentami staje siÄ? tak proste jak wype??nianie formularzy. KorzystajÄ?c z przyk??adowego dope??nienia zdefiniowanego przez autora modu??u, po wpisaniu fopenx i wci??niÄ?ciu spacji, zostanie wklejony w aktualne miejsce kod podobny do nastÄ?pujÄ?cego:
if ((f = fopen(“nazwa_pliku”, “r”)) == NULL)
{
std::cerr << “ERROR: opening file”
<< ” at “ << __FILE__ << “:” << __LINE__
<< std::endl << std::flush;
exit(1);
}
Pola, kt??rymi r????niÄ? siÄ? wywo??ania fopen mo??na wype??niaÄ? jak pola formularza, przechodzÄ?c od jednego do drugiego przy pomocy klawisza Tab. Je??eli chcecie zobaczyÄ? modu?? w akcji, na stronie projektu dostÄ?pne jest demo.
Chocia?? msf-abbrev czÄ?sto siÄ? przydaje, stwarza powa??ne zagro??enie dla zasady DRY. Podany wy??ej przyk??ad pisze siÄ? bardzo prosto, jego usuniÄ?cie lub modyfikacja jest ju?? jednak bardzo k??opotliwa. Wyobra??cie sobie bowiem, ??e musicie zmieniÄ? w komunikacie b??Ä?du “ERROR” na “Error”, lub te?? kod wyj??cia z jedynki na dw??jkÄ?. Z pomocÄ? przyj??Ä? mo??e edytor z dobrÄ? obs??ugÄ? operacji zastÄ?powania, jednak samo nawet okre??lenie, w kt??rych plikach (mo??liwe, ??e rozsianych po r????nych projektach) u??yli??my tych szablon??w, mo??e byÄ? bardzo czasoch??onne. Tymczasem rozwiÄ?zanie dla podanego przyk??adu mo??na napisaÄ? w samym C++, trzymajÄ?c w ten spos??b definicjÄ? w jednym centralnym punkcie.
#define fopenx(filename, mode) \
_fopenx(filename, mode, __FILE__, __LINE__)
FILE *_fopenx(char *filename, char *mode, char *file, int line) {
FILE *f = NULL;
if ((f = fopen(filename, mode)) == NULL) {
std::cerr << “ERROR: opening file”
<< ” at “ << file << “:” << line
<< std::endl << std::flush;
exit(1);
}
return f;
}
U??ywanie funkcji fopenx() jest bardzo proste:
f = fopenx(“nazwa_pliku”, “r”);
W ten spos??b nie ma potrzeby siÄ?gaÄ? po msf-abbrev, bo znak??w do wpisania nie jest wcale a?? tak wiele.
Mo??liwo??ci abstrakcji w Scheme sÄ? naprawdÄ? potÄ???ne. DziÄ?ki temu, ??e funkcje sÄ? pe??noprawnymi obiektami i mo??na nimi dowolnie manipulowaÄ?, z przekazywaniem i zwracaniem w??Ä?cznie, mogÄ? s??u??yÄ? one nie tylko do opisu konkretnych dzia??a?? (jak np. obliczenie iloczynu dw??ch argument??w), ale i do og??lnych sposob??w rozwiÄ?zywania szerokich klas problem??w (np. metoda Newtona znajdowania pierwiastk??w funkcji). DziÄ?ki czystej sk??adni w miarÄ? przyrostu z??o??ono??ci kod nie staje siÄ? brzydki. Makra sÄ? za?? furtkÄ? do czynienia rzeczy najprostszymi jak to tylko mo??liwe. Przyk??ad? ProszÄ? bardzo. PiszÄ?c w Scheme czÄ?sto korzysta siÄ? z funkcji rekurencyjnych, by jednak wywo??aÄ? siebie, muszÄ? byÄ? one nazwane. Wymaga to u??ycia form specjalnych define lub let, co wymusza okre??lonÄ? kolejno??Ä? wyra??e?? (najpierw definicje, potem u??ycie). Czasami jednak chcieliby??my mieÄ? mo??liwo??Ä? zdefiniowania anonimowej funkcji, kt??ra jednocze??nie wywo??uje samÄ? siebie. Inaczej - chcieliby??my po??Ä?czyÄ? si??Ä? form lambda i define. W Scheme istnieje ju?? forma specjalna, kt??ra tworzy nowÄ? zagnie??d??onÄ? przestrze?? nazw, pozwalajÄ?c na umieszczenie w niej nowych definicji. Jest to let. Jej sk??adnia jest, jak ka??dej formy specjalnej, bardzo prosta:
(let ((<nazwa1> <wyrazenie1>)
(<nazwa2> <wyrazenie2>)
…)
<wartosc>)
Ka??dej podanej nazwie zostanie przyporzÄ?dkowana odpowiadajÄ?ca jej warto??Ä?, za?? ca??e wyra??enie let zwr??ci warto??Ä? wyra??enia podanego jako drugi argument. Ze zdefiniowanych nazw mo??na korzystaÄ? tylko w ostatnim wyra??eniu. My definiujemy funkcjÄ? rekurencyjnÄ?, musimy wiÄ?c mieÄ? mo??liwo??Ä? skorzystania z nazw jeszcze w samych wyra??eniach. PozwalajÄ?ca na to odmiana let nazywa siÄ? letrec.
Rozwa??my prosty przyk??ad silni. Dotychczas, by obliczyÄ? silniÄ? z dowolnej liczby musieli??my zdefiniowaÄ? obliczajÄ?cÄ? jÄ? procedurÄ?, a dopiero po tym z niej skorzystaÄ?.
(define (silnia x)
(if (= x 0)
1
(* x (silnia (- x 1)))))
(silnia 5)
WykorzystujÄ?c letrec mo??emy powy??szy efekt uzyskaÄ? piszÄ?c:
((letrec ((silnia
(lambda (x)
(if (= x 0)
1
(* x (silnia (- x 1)))))))
silnia) 5)
Ano, piÄ?knie, jedyny mankament to wyglÄ?d. Definicja nie wyglÄ?da zbyt dobrze - s??owa silnia u??ywamy dwa razy (w definicji i nastÄ?pnie jako warto??Ä? wyra??enia letrec), gdy wystarczy??o by raz, za?? w miejsce lambda i letrec mo??na by wstawiÄ? pojedynczÄ? nazwÄ?. Spr??bujmy wiÄ?c to poprawiÄ?. Niestety nie da siÄ? wyabstrahowaÄ? powy??szego kodu do funkcji, g????wnie przez to, ??e Scheme korzysta z zasiÄ?gu leksykalnego (ang. lexical scope). Najkorzystniej by??oby, gdyby??my mogli okre??lone wyra??enie przekszta??ciÄ? w inne, jeszcze zanim nastÄ?pi jego wykonanie. Do tego w??a??nie s??u??Ä? makra. Okre??lmy najpierw, jak chcieliby??my, by nasza nowa lambda wyglÄ?da??a. Ja sk??ania??bym siÄ? ku sk??adni przypominajÄ?cej define, tzn:
(def-lambda (<nazwa funkcji> <argumenty>)
<zwracana wartosc>)
Chocia?? wyglÄ?da identycznie, r????ni siÄ? od define tym, ??e zamiast zwiÄ?zywaÄ? funkcjÄ? z podanÄ? nazwÄ? w aktualnej przestrzeni nazw, zwraca tÄ? funkcjÄ? jako warto??Ä?. Nasz przyk??ad z silniÄ? upro??ci??by siÄ? wiÄ?c do nastÄ?pujÄ?cej postaci:
((def-lambda (silnia x)
(if (= x 0)
1
(* x (silnia (- x 1))))) 5)
Tak, to wyglÄ?da o wiele ??adniej. Nie wg??Ä?biajÄ?c siÄ? zbytnio w sk??adniÄ?, definicjÄ? makra def-lambda jest nastÄ?pujÄ?ca:
(define-syntax def-lambda
(syntax-rules ()
((_ (self . args) value)
(letrec ((self (lambda args value)))
self))))
Wyja??nienie dzia??ania makr to temat na zupe??nie inny wpis (a w zasadzie na kilka wpis??w), chcia??bym jednak zwr??ciÄ? uwagÄ? na sam efekt ich u??ycia. DziÄ?ki wbudowanym w jÄ?zyk mo??liwo??ciom uda??o nam siÄ? uog??lniÄ? powtarzajÄ?cy siÄ? kod do zwartej i zrozumia??ej postaci, kt??ra ma dodatkowo tÄ? piÄ?knÄ? w??a??ciwo??Ä?, ??e niczym nie r????ni siÄ? od innych form specjalnych. W pewnym sensie zmienili??my wiÄ?c sam jÄ?zyk w taki spos??b, by bardziej odpowiada?? naszym potrzebom. PiÄ?kno tego rozwiÄ?zania bije na g??owÄ? to, co oferuje msf-abbrev. Dlatego, je??eli tylko macie okazjÄ?, zamiast zmieniaÄ? edytor, proponujÄ? zmieniÄ? jÄ?zyk programowania.