Implementacja identikon w PLT Scheme
czerwiec 6th, 2008 at 5:10 pm (scheme, plt)
Gdy w sieci przeczytałem o identikonach o wiele bardziej urzekła mnie estetyka ich wyglądu niż ich techniczne zalety. Identikony, w formie zaimplementowanej przez Dona Parka, służyć mają jako graficzna reprezentacja tożsamości osoby w sieci. Założenie jest takie, że kolorowe wzory są łatwiejsze do porównania i zapamiętania przez człowieka, niż numer IP. Dodatkowo, przepuszczając IP użytkownika przez funkcję haszującą z sekretnym ziarnem możemy zachować właściwości identyfikujące bez potrzeby ujawniania samego numeru IP.
Będąc zainteresowany samymi wzorami, szybko odnalazłem opis
sposobu ich generowania i zacząłem zastanawiać się nad
implementacją. Pomyślałem bowiem, że to świetna okazja na wypróbowanie
możliwości Scheme i bliższe poznanie wybranego środowiska. Padło na PLT, głównie ze względu na moją
wcześniejszą z nim styczność i dobre doświadczenia wyniesione z eksperymentów w
DrScheme.
Jak się później dowiedziałem, bloczki, z których zbudowane są identikony, zostały początkowo opisane przez Jareda Tarbella na podstawie mającej długą historię sztuki quiltu. A to z kolei pamiętam, że gdzieś już widziałem.
Przede wszystkim zachęcam do ściągnięcia i wypróbowania kodu. Do prawidłowego działania wymaga on PLT z serii 3.99 (lub 4.0, który ma wyjść całkiem niedługo). Poniżej zaś kilka komentarzy i wniosków, które nasunęły mi się podczas implementacji.
Bibliotekę wrzuciłem na Planetę PLT, by jej użyć wystarczy więc jedna linijka kodu:
(require (planet “identicons.ss” (“mk” “identicons.plt”)))
Implementacja identikon zostanie automatycznie ściągnięta i zainstalowana.
System obiektowy
System obiektowy PLT Scheme funkcjonalnie nie różni się bardzo od tych
zawartych w językach takich jak Java, Python, czy Ruby. Filozofia jest
jednak trochę inna, głównie z tej przyczyny, że system obiektowy nie
jest podstawą języka, ale jednym z wielu sposobów organizacji programu.
Po załadowaniu modułu scheme/class nie doświadczymy więc
wielkich zmian. Podstawowe typy danych, takie jak liczby, symbole, czy
ciągi znaków nie zostają wciągnięte w hierarchię klas. Prawdę mówiąc,
poza klasą
object%
i interfejsem
externalizable<%>
hierarchia ta jest całkiem pusta i zadanie wypełnienia jej leży
w rękach programisty. Po bibliotekach standardowych pękających od
podstawowych klas i obiektów jest to całkiem odświeżające doświadczenie,
muszę przyznać. W założeniu twórców PLT obiekty mają służyć do sprawnego
zamodelowania dziedziny problemu, przed jakim staje programista. A do
tego nie potrzeba bagażu historycznego jaki często niosą ze sobą hierarchie
starszych języków obiektowych.
Podstawowe konstrukcje są dość dobrze opisane w dokumentacji, a pomiędzy liniami można też poznać kilka konwencji. Nazwy klas kończą się %, a nazwy interfejsów <%>. W obrębie wyrażenia definiującego klasę dostępnych jest kilka form specjalnych służących do definiowania atrybutów i metod. Do komfortowej pracy wystarczy znajomość czterech z nich:
- init-field do definiowania atrybutów, które może ustawić użytkownik podczas tworzenia obiektu
- field do definiowania pozostałych atrybutów
- define/public do definiowania publicznych metod
- define do definiowania metod prywatnych
Dla przykładu przyjrzyjmy się implementacji identikony:
(define identicon%
(class object%
(init-field seed)
(field (32-bits (make-bit-stream seed))
(center-patch-shape (list-ref center-patch-shapes (32-bits 2)))
(side-patch-shape (list-ref patch-shapes (32-bits 4)))
…)
(define (get-patch-size dc)
…)
(define/public (draw dc)
…)
(super-new)))
Kod zdaje się mówić sam za siebie. seed jest atrybutem
ustawianym podczas konstrukcji obiektu. 32-bits,
center-patch-shape i side-patch-shape
są ukrytymi atrybutami przyjmującymi zadane wartości. Warte zauważenia
jest to, że 32-bits zależy od atrybutu seed,
podczas gdy samo 32-bits jest używane w następujących
po nim definicjach. get-patch-size jest metodą dostępną
tylko dla pozostałych metod tej klasy (czyli jest metodę prywatną),
a draw jest metodą publiczną. Wywołanie (super-new)
kończące definicje klasy jest potrzebne do właściwej inicjalizacji
obiektu przez nadklasę object%.
Konstrukcja obiektów jest bezbolesna. Przykładowo, by utworzyć nową identikonę wystarczy napisać:
(make-object identicon% 1234567890)
i dostaniemy z powrotem identikonę pokazaną poniżej. By zapisać identikonę
do pliku, wystarczy wywołać na niej metodę save-to-file z podaniem
nazwy pliku i rozmiarem identikony w pikselach:
(send identicon save-to-file “1234567890.png” 200)
O ile do tego momentu nie miałem większych zastrzeżeń do systemu obiektowego
PLT to do sposobu wywoływania metod muszę się już przyczepić. :-)
System obiektowy z przekazywaniem komunikatów (ang.
message passing)
moim zdaniem idzie trochę na przekór lispowej filozofii. Common Lispowe procedury
ogólne (ang. generic procedures)
nie dość, że potężniejsze, to moim zdaniem bardziej idą z duchem Lispa. Pisanie kodu
podobnego do:
(send identicon draw 30)
wydaje się nienaturalne, umieszczając nazwę wykonywanej operacji dopiero na
trzeciej pozycji wyrażenia. Naturalną składnią dla wołania metody draw
jest następująca postać powyższego wyrażenia:
(draw identicon 30)
Co prawda, można by zacząć dodawać dla wszystkich metod publicznych odpowiadającą im procedurę, np.:
(define (draw object . args)
(send/apply object draw args))
i może nawet powiązać ten efekt z define/public, ale wtedy
mielibyśmy już nowy system obiektowy, łamiący świętą w świecie Scheme
makrową higienę.
Oczywiście świat nie jest czarno-biały i istnieją zalety podejścia
używanego przez PLT. Jednym z niewątpliwych plusów jest fakt, że jedynym
symbolem, jaki trzeba eksportować z modułu jest nazwa klasy - nazwy
metod bowiem rozwijane są jedynie w kontekście konkretnego obiektu.
Metody “podążają za” obiektami, co w systemie z przekazywaniem komunikatów
ma sens.
W PLTowym pakiecie Swindle jest dostępny system obiektowy bliższy Common Lispowemu CLOSowi, będzie więc jeszcze okazja przyjrzeć się innemu podejściu.
Inną ciekawostką związaną ze standardowym PLTowym systemem obiektowym jest to, że klasy same nie są obiektami:
(object? identicon%) ;; => #f
Nie możemy więc na nich wywoływać metod i definiować atrybutów. Nie jest to jednak potrzebne, w PLT istnieją bowiem moduły, w których możemy zawrzeć zarówno lokalny stan, jak i grupę procedur. O zabawach z metaklasami możemy jednak zapomnieć (co prawdopodobnie językowi wychodzi na zdrowie ;-).
Moduły i kontrakty
Wraz z nadejściem R6RS doczekaliśmy się standardowego wsparcia dla modułów (bibliotek) w Scheme, zanim jednak ten czas nastąpił każda z implementacji musiała rozwiązać ten problem po swojemu. Moduły w PLT, w ograniczonym zakresie jakim miałem okazję ich używać, spełniają swoją funkcję doskonale. By obudować całą zawartość pliku w moduł wystarczy dodać na jego początku linijkę z określeniem używanego podzbioru języka przy pomocy #lang. Przykładowo, by użyć języka rozszerzonego o możliwości operowania oknami i grafiką należy użyć:
#lang scheme/gui
Osiągamy w ten sposób efekt podobny do modułów pythonowych - każdy plik stanowi moduł, którego nazwa wywodzi się bezpośrednio z nazwy pliku, z pominięciem rozszerzenia. Plik identicons.scm zawiera więc moduł o nazwie identicons.
Prócz tej jednej linijki moduł prawdopodobnie nie będzie wymagał żadnych zmian.
require
wciąż działa, a wszystkie definicje są szczelnie zamknięte dla zewnętrznego
świata. Udostępnianie symboli na zewnątrz odbywa się przy pomocy formy
specjalnej
provide,
ja jednak od pierwszego wejrzenia zakochałem się w formie
provide/contract.
Przy jej pomocy bowiem możemy nie tylko określić eksportowane symbole, ale
również określić
kontrakt,
który będzie obowiązywał pomiędzy modułem, a jego użytkownikiem. Przykładowo,
kontrakt funkcji degrees->radians zdefiniowanej w module
util wygląda następująco:
[degrees->radians (number? . -> . number?)]
Oznacza on, że degrees->radians jest funkcją, która oczekuje
liczbę jako argument i obiecuje zwrócić liczbę. Proste. Jak widać, kontrakty
mogą służyć do dynamicznego sprawdzania typów, ale ich możliwości sięgają
daleko poza. Kontrakt można zbudować z dowolnego predykatu, a te mogą
sprawdzać nie tylko typ, ale i wartości i stan obiektów przekraczających
granice kontraktu. Świetne w kontraktach jest to, że są całkowicie opcjonalne.
Nie ma więc żadnego problemu w tym, by dojrzalsze części budowanego przez nas
systemu posiadały kontrakty i jednocześnie współpracowały z nowszymi modułami,
których projekt wciąż jest niejasny - a przez to - pozbawiony kontraktów. Poza
oczywistymi względami technicznymi kontrakty posiadają również niewątpliwą
wartość dokumentacyjną. Odpowiednio dobrane nazwy predykatów mogą wiele
powiedzieć o oczekiwaniach, jakie kładziemy na argumenty funkcji
i o obietnicach jakie składamy co do wartości przez te funkcje zwracanych.
Co ciekawe, kontrakty można stosować nie tylko w kontekście modułów, ale również bezpośrednio do definicji, jak i do metod i atrybutów obiektów.
Składnia infixowa
Jak wspomina przypis na stronie dokumentacji kontraktów PLT Scheme udostępnia specjalną składnię z dwoma kropkami. Jej działanie jest bardzo proste. Każde wyrażenie o postaci:
(a b . x . c d)
przekształcane jest na:
(x a b c d)
jeszcze przed interpretacją. Oprócz tego, że składnia ta jest bardzo przydatna przy definiowaniu kontraktów, można ją (nad)użyć chociażby do pisania wyrażeń algebraicznych. Przykładowo:
(a . * . (b . + . c))
jest tym samym co:
(* a (+ b c))
Osobiście jednak odradzałbym posuwanie się z użyciem tego składniowego lukru zbyt daleko. ;-)
Narzędzia graficzne
Moje zainteresowanie grafiką w PLT zaczęło się od tego wspaniałego tutoriala wstępnego. Polecam go tym, którzy chcą zacząć przygodę z Lispem, ale nudzi ich operowanie wyłącznie na liczbach. A w zasadzie to polecam go każdemu, tak dobry jest. :-)
Niestety do implementacji identikon
biblioteka prezentacyjna
jest niewystarczająca. Głównym jej minusem jest brak możliwości obracania
obiektów graficznych. Indeks szybko jednak zaprowadził mnie do
metody rotate klasy dc-path%,
która to stanowi część
większej biblioteki graficznej
dostępnej wraz z PLT.
Klasa dc-path% umożliwia
narysowanie, wypełnienie i manipulacje dowolnym kształtem, złożonym z odcinków,
czy łuków. Konstrukcja takiej ścieżki jest bardzo prosta. Przykładowo, by narysować
trójkąt najpierw tworzymy nowy obiekt dc-path%:
Cały poniższy kod musi być uruchamiany z linii poleceń mred lub w środowisku drscheme; funkcjonalność graficzna nie jest dostępna z poziomu mzscheme.
(define path (make-object dc-path%))
Po tym określamy punkt startowy (”punkt przyłożenia ołówka do kartki”):
(send path move-to 0 0)
i zaczynamy rysować jedną z dostępnych metod, przykładowo line-to:
(send path line-to 100 100)
(send path line-to 0 100)
(send path line-to 0 0)
Żeby zobaczyć efekt musimy przygotować odpowiedni kontekst do rysowania (ang. drawing context), czyli w praktyce cokolwiek, co implementuje interfejs dc<%>. Skorzystajmy z kodu identikon i użyjmy do tego celu zwykłej ramki (czy też “okna”):
(define (make-frame-dc)
(let* ((frame (new frame% (label “New frame”) (width 200) (height 200)))
(canvas (new canvas% (parent frame))))
(send frame show #t)
(sleep/yield 1)
(send canvas get-dc)))
Utwórzmy więc właściwy obiekt i narysujmy na nim nasz trójkąt:
(define dc (make-frame-dc))
(send dc draw-path path)
W tym momencie na ekranie powinniśmy zobaczyć zarys trójkąta. Pokolorowanie go to tylko kwestia użycia odpowiedniego pędzla. Wybierzmy dla niego jakiś pozytywny kolor i przerysujmy nasz trójkąt ponownie (oczywiście wszystko bez restartowania interpretera, czy otwierania nowej ramki):
(send dc set-brush “green” ’solid)
(send dc draw-path path)
Gotowe, nasz trójkąt jest teraz zielony. A stąd już niedaleko do generowania kolorowych identikon. Po szczegóły zapraszam do źródeł.