DRY

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 (( )
( )
…)
)

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

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.

Escape Meta Alt Control Shift

Ten wpis miał być o DrScheme, środowisku do tworzenia i testowania programów napisanych w języku Scheme. Miałem go skończyć już kilka dni temu. Tymczasem od ostatniej notki mija już tydzień, a mi wciąż ciężko zebrać się do pisania. Powód?

Emacs.

Pozwolę sobie zacytować najpierw pewnego początkującego emacsowca, którego wypowiedź najłatwiej opisze moją obecną sytuację:

“Jak ze wszystkimi rzeczami w Uniksie również ten to przypadek dalekiej podróży zaczynającej się jednym niepewnym krokiem.”

“I suppose, as with all things UNIX, its a case of the journey of a thousand miles beginning with one step.”

Do DrScheme prawdopodobnie jeszcze wrócę, posiada on bowiem ciekawe narzędzie do debugowania kodu. Wiele skrótów klawiszowych jest w DrScheme taka sama jak w Emacsie, więc ewentualne przejście będzie mało bolesne (po tym co do tej pory widziałem wątpię jednak, by ktoś chciał rezygnować z Emacsa na rzecz DrScheme).

Emacs - edytor tekstu

Opiszę więc teraz pokrótce wszystko to, co udało mi się w Emacsie okryć przez te kilka dni. Rady będą zorientowane na systemy uniksowe, ale jestem pewny, że użytkownicy Windowsów wykażą się charakterystycznym dla siebie samozaparciem i bez problemu sobie poradzą. ;-) Emacsa ściągnąć można ze strony GNU. Osobiście polecam najnowsze wersje z cvs’a. W Debianie rozbite są one na paczki emacs-snapshot, emacs-snapshot-bin-common, emacs-snapshot-common, i emacs-snapshot-el. Po wpisaniu w terminalu emacs edytor włącza się w okienku. Ja zdecydowanie wolę pracę na konsoli, więc ustawiłem sobie w moim .bashrc alias:

alias emacs='emacs-snapshot -nw'

No dobrze, wiemy jak włączyć, ale jak tego cuda używać? Przede wszystkim, zanim nawet zaczniecie, proponuję każdemu podpiąć pod Caps Lock klawisz Ctrl. Mając podstawowy klawisz pod małym lewym palcem naprawdę ułatwia edycję. Najlepiej od razu przeczytajcie sobie ten tutorial, gdzie oprócz tej rady podanych jest wiele innych, których stosowanie przekłada się na wymierne polepszenie komfortu pracy z Emacsem.

Podstawy edycji załapać możecie czytając poradnik Marcina Bielewicza. Ja opiszę Emacsa z punktu widzenia kilku dni używania ze zwróceniem uwagi na walory ułatwiające dłubanie w kodzie Scheme.

Jedną z podstawowych koncepcji Emacsa są bufory, które reprezentują pewne wirtualne przestrzenie do pracy. Przypomina to trochę ideę uniksowych terminali. Bufor może być związany z plikiem, gdy właśnie go edytujemy/przeglądamy. Ale w buforze możemy też trzymać otwartego shella, sesję ssh, interpreter pythona, czy cokolwiek innego. Nie było by w tym jednak nic ciekawego, gdyby nie fakt, że ze wszystkich buforów można korzystać przy pomocy tych samych poleceń edycyjnych. Nie ma więc żadnego problemu, by skopiować z powłoki kilka linijek, albo przeszukać okno interpretera za pomocą znajomej kombinacji C-s (notacja ta oznacza: trzymając Ctrl wciśnij s). Poprzednie/następne polecenie uzyskuje się przy pomocy kombinacji C- strzałka w górę/w dół. I tak dalej, możliwości są naprawdę niezliczone, wszystko dzięki ujednoliceniu zachowania buforów.

Pomiędzy buforami poruszać się można kolejno przy pomocy kombinacji C-strzałka w lewo/w prawo. Na raz widocznych może być kilka buforów. Okno dzieli się przy pomocy kombinacji C-x 2 (wybierz Ctrl+x, a potem 2). By zmaksymalizować aktywny bufor, należy wpisać C-x 1, by go ukryć: C-x 0. Bufor zamyka się kombinacją C-x k. Zazwyczaj przed zamknięciem bufora związanego z plikiem chcesz wszystkie swoje zmiany zachować. Służy do tego kombinacja C-x C-s.

Komenda, która wciąż ratuje mi życie, to C-g, aka keyboard-quit. Powoduje ona zaprzestanie czynności, którą właśnie wykonuje edytor. Nadal często zdarza mi się wklepać przypadkiem dowolną kombinację klawiszy, po której nie za bardzo wiem gdzie jestem. C-g zawsze wraca do stanu pierwotnego.

I w zasadzie tyle wystarczy do w miarę sprawnego korzystania z edytora. Swoje umiejętności można udoskonalić czytając wbudowany tutorial dostępny pod kombinacją klawiszy C-h t. Odważni mogą też zacząć czytać dokumentację.

Emacs - środowisko programistyczne

To, że bufory zachowują się podobnie, to tylko jedna strona medalu. Właściwości bufora determinuje również tryb (ang. mode) w jakim się on znajduje. Domyślny bufor *scratch* rozpoczyna pracę w trybie Lisp Interaction. Jest to tryb, w którym wpisane w Lispie polecenia możemy przesłać Emacsowi do wykonania. Spróbujcie na przykład wpisać:

(menu-bar-mode -1)

Mając kursor na końcu linii wybierzcie kombinację klawiszy C-x C-e. Wykona ona podane polecenie, powodując zniknięcie menu. By je przywrócić wystarczy zamienić argument funkcji na 1 i wykonać kod ponownie. To tylko prosty przykład; warto być świadomym, że do dyspozycji mamy kompletny interpreter Lispa (a dokładnie dialektu nazywanego Emacs Lispem).

Emacs posiada tryb dla Scheme (nazywa się on po prostu scheme-mode), istnieje jednak ciekawa biblioteka rozszerzająca jego możliwości. Jest to Quack, dostępny w Debianie w pakiecie quack-el. Po instalacji wystarczy dodać jedną linijkę do pliku ~/.emacs i jesteśmy gotowi do pracy:

(require 'quack)

Otwieramy Emacsa, naszym oczom ukazuje się standardowy ekran z przywitaniem. Wciskamy C-x b i wpisujemy test, tworząc w ten sposób nowy bufor o nazwie test. Jako, że chcemy pisać program w języku Scheme, uruchomimy w buforze odpowiedni tryb: M-x scheme-mode. Quack automatycznie podłącza się pod ten tryb, dopisana do ~/.emacs pojedyncza linijka zajęła się wszystkimi ustawieniami. Możemy zacząć pisać. Warto zwrócić uwagę na kolorowanie składni, automatyczne wstawianie wcięć i skaczący kursor dopasowujący nawiasy. Po napisaniu kodu zapewne chcielibyśmy go przetestować. Interpreter Scheme uruchamiamy kombinacją C-c C-q r (lub też M-x run-scheme). By upewnić się, że interpreter działa poprawnie, możemy wpisać dowolne poprawne wyrażenie:

> (+ (* 2 3) 4)
10

No dobrze, ale nie chcemy przecież przepisywać w interpreterze całego kodu, który właśnie napisaliśmy w oknie edycyjnym. Nie ma problemu, Emacs świetnie radzi sobie z kopiowaniem tekstu pomiędzy oknami. Przechodzimy do okna edycji kombinacją C-x o. Zaznaczamy cały bufor kombinacją C-x h, a kopiujemy go do interpetera przy pomocy C-c C-r. Gotowe. Załóżmy, że w oknie edycyjnym mieliśmy rekurencyjną definicję silni.

(define (silnia x)
(if (= x 0)
1
(* x (silnia (- x 1)))))

Przechodząc do okna intepretera możemy już z właśnie wykonanego kodu skorzystać. Piszemy:

(silnia 5)

I otrzymujemy poprawny wynik:

120

Oczywiście to nie koniec atrakcji. Co powiecie na szybki dostęp do pełnej dokumentacji języka Scheme? Po wybraniu M-x quack-view-manual i wciśnięciu Tab przedstawione wam zostaną nazwy wszystkim manuali, jakie są dostępne online. Wystarczy wpisać nazwę dowolnego z nich i potwierdzić, by Emacs otworzył przeglądarkę na odpowiedniej stronie. U mnie włącza się Mozilla, co nie jest najszczęśliwszym rozwiązaniem. Ale Emacs nie byłby nazywany złośliwie systemem operacyjnym, gdyby nie pozwalał na uruchomienie w buforze przeglądarki. Ja używam w3m, której emacsowy pakiet w Debianie to w3m-el. By zmienić używaną przez Quack przeglądarkę trzeba przejść do bufora *scratch* i wpisać następującą linijkę:

(setq quack-browse-url-browser-function 'w3m-browse-url)

Wykonujemy ją zwyczajowo przy pomocy C-x C-e. Jeżeli ponownie otworzymy dowolny manual, zobaczymy go już bezpośrednio w Emacsie. No dobrze, ale możemy czytać dokumentację i bez Emacsa. Quack oferuje nam coś więcej. Jeżeli chcecie uzyskać dokumentację dla dowolnej funkcji, wystarczy, że wpiszecie jej nazwę i wykonacie kombinację C-c C-q k. Jest jednak błąd, nie wiem nawet czy w module w3m, czy w quacku (skłaniałbym się ku pierwszemu, bo z innymi przeglądarkami nie ma problemów), który powoduje, że przeglądarka nie skacze do opisu funkcji. Po dłuższych bojach udało mi się rozwiązać problem. Dodajcie poniższe linijki do swojego pliku ~/.emacs, a w3m będzie już działać poprawnie:

(defun substitute-hex-values (url)
(if (string-match "%25" url)
(replace-match "%" nil nil url)
url))

(defun my-w3m-browse-url (url &optional new-session)
(when (stringp url)
(w3m-browse-url (substitute-hex-values url) new-session)))

(setq quack-browse-url-browser-function 'my-w3m-browse-url)

Jak widzicie, możliwości Emacsa są całkiem spore. Pozwolę sobie wspomnieć o jeszcze jednej często używanej kombinacji klawiszy, jaką jest M-/. Dopełnia ona właśnie pisane słowo na podstawie innych słów w tym buforze (a także w innych buforach). Przykładowo, jeżeli zdefiniowaliście wcześniej funkcję funkcja-o-długiej-nazwie, to wystarczy, wpisać kilka pierwszych liter i użyć kombinacji M-/, by Emacs oszczędził nam dalszego pisania.

Jeżeli czujecie niedosyt, zapraszam do eksperymentowania z Emacsem. Możecie też przejrzeć tutoriale o Scheme w Emacsie: tutaj i tutaj.

.emacs

Każde polecenie, które wykonywaliśmy w buforze *scratch* możemy zapisać do pliku .emacs, dzięki czemu zostanie ono wykonane zaraz po uruchomieniu edytora. Opiszę poniżej kilka fragmentów mojego pliku .emacs, które mogą wam się przydać.


(column-number-mode 1)

Powoduje, że w pasku statusu widoczny jest nie tylko numer linii, ale i numer kolumny.


(setq inhibit-startup-message t)

Dzięki temu po włączeniu Emacsa nie pojawia się ekran z przywitaniem.


(setq make-backup-files nil)

Wyłącza tworzenie kopii zapasowych edytowanych plików. Bez tego w krótkim czasie katalogi zapełniają się masą plików z tyldą na końcu.


(setq truncate-lines t)

(defun point-of-beginning-of-bottom-line ()
(save-excursion
(move-to-window-line -1)
(point)))

(defun point-of-beginning-of-line ()
(save-excursion
(beginning-of-line)
(point)))

(defun next-one-line () (interactive)
(if (= (point-of-beginning-of-bottom-line)
(point-of-beginning-of-line))
(progn (scroll-up 1)
(next-line 1))
(next-line 1)))

(defun point-of-beginning-of-top-line ()
(save-excursion
(move-to-window-line 0)
(point)))

(defun previous-one-line () (interactive)
(if (= (point-of-beginning-of-top-line)
(point-of-beginning-of-line))
(progn (scroll-down 1)
(previous-line 1))
(previous-line 1)))

(global-set-key (kbd "“) ‘next-one-line)
(global-set-key (kbd ““) ‘previous-one-line)

(defun scroll-down-by-1 () (interactive)
(scroll-down 1))
(defun scroll-up-by-1 () (interactive)
(scroll-up 1))
(global-set-key “\C-n” ’scroll-up-by-1)
(global-set-key “\C-p” ’scroll-down-by-1)

Kod pożyczony stąd. Powoduje on płynne przewijanie ekranu przy korzystaniu ze strzałek. Ja dodałem obsługę klawiszy C-n i C-p, które powodują przewinięcie ekranu w górę/w dół bez zmieniania pozycji kursora.


(setq quack-run-scheme-always-prompts-p nil)

Wpisanie w scheme-mode kombinacji C-c C-q r powoduje natychmiastowe uruchomienie interpretera Scheme bez pytania o jego nazwę.


(setq quack-pretty-lambda-p 1)

Jeżeli posiadacie odpowiedni terminal z odpowiednią czcionką zamiast słowa lambda widoczna będzie grecka litera.

Jeżeli znacie jakąś funkcję Emacsa, która przydaje się przy edycji kodu, zachęcam do opisania jej w komentarzach.

« Wcześniejsze wpisy ·