"The Grid. A digital frontier. I tried to picture clusters of information as they move through the computer. What did they look like? Ships? Motorcycles? Were the circuits like freeways? I kept dreaming of a world I thought I’d never see. And then, one day, I got in." — Tron: Legacy

2012-09-30

Smalltalk : Seaside z użyciem Pharo, cz.2

(c) Robert Tinney

Dzisiaj postanowiłem pokazać jak w prosty sposób odczytać komentarze z bloga na portalu dobreprogramy.pl, który nie posiada usługi odczytu wiadomości za pomocą XML/JSON przez co trzeba parsować HTML. Dla niecierpliwych na końcu tekstu jest krótki film podsumowujący temat tego wpisu.

Odczyt zawartości strony internetowej robi się dość banalnie. Wystarczy w Workspace Pharo wpisać :

'http://www.google.com' asUrl retrieveContents contents.
 
Potem umieścić kursor klawiatury za kropką (bądź zaznaczyć myszką całą linijkę kodu) i nacisnąć [Ctrl+p] ("Print it"). Powinna pojawić się zawartość z podanego adresu. Tekst, który się pojawia jest już zaznaczony, więc można nacisnąć klawisz Del by go usunąć.

Jako, że tekst strony już mamy, to potrzebny będzie parser HTML. Będzie potrzebny, bo wbudowany parser XML tonie w zupie tagów luźno zapodanego HTMLa, gdzie zdarzają się niedomknięte tagi, ew. tagi zapisane niezgodnie z dokumentacją i wtrącenia w postaci JavaScriptu czy CSS i inne śmieci.

Dodatkowe biblioteki Smalltalka są przechowywane w repozytoriach. Z takiego dziś skorzystam. Jest to repozytorium Squeaka (inna odmiana Smalltalka zgodna z Pharo).

Aby zainstalować bibliotekę trzeba wejść do przeglądarki Monticello (dostępne z menu myszki). Następnie trzeba kliknąć w "+Package" nadać dowolną nazwę, np. "Soup". Potem klik w "+Repository" i wybieram opcję HTTP. Modyfikuję adres, który się pojawi na szablonie na:

location: 'http://www.squeaksource.com/Soup'

Klikam OK, potem w "Open". Załaduje się lista wersji pakietów. Trzeba teraz wybrać ConfigurationOfSoup - najnowszą wersję , u mnie jest 37 i kliknąć Load. ConfigurationOfSoup to pakiet obiektów, które pobiorą najnowszą wersję zupki. Aby się dowiedzieć jak go uruchomić trzeba otworzyć System Browser i znaleźć ConfigurationOfSoup. Można to zrobić poprzez menu z Find, górną belkę, lub przewinąć listę na sam koniec. Najnowsze pakiety są widoczne zawsze na końcu. W drugim okienku, które jest zwane klasowym lub instancji trzeba kliknąć jeszcze raz nazwę ConfigurationOfSoup, a potem pytajnik. Pod pytajnikiem zwykle jest dokumentacja obiektu.

Widać tam dwie linijki. Jako, że kod Smalltaka można wykonać z każdego okienka, zaznaczam linijkę z wersją #stable i wykonuję "Do it". skrypt pobierze i skompiluje resztę biblioteki.

Teraz można pobrać zupę do obiektu już za pomocą samej biblioteki (oczywiście wszystko to wpisuję w Workspace):

zupa := Soup fromUrl: 'http://*dobreprog***.pl/aeroflyluby/Domowy-sposob-na-Diode,36474.html'.

Tym razem użyłem prawdziwego adresu z bloga. Mam więc zupę z tagów i w tym momencie zaczyna się zabawa. Trzeba dowiedzieć się pomiędzy jakimi tagami trzymany jest blok komentarzy. Potem znaleźć pojedyncze wpisy, a następnie rozbić na:
  •     adres logo: #img
  •     nazwę komentującego: #nick
  •     treść komentarza: #text
Zrobiłem małe rozeznanie i znalazłem id tagu, w którym te komentarze siedzą:

ctl00_phContentLeft_panUpdateComment
 
Niestety Soup nie ma możliwości wyciągania tagów po identyfikatorze. Trzeba sobie napisać swoją funkcję. Najlepiej to zrobić zrzynając już z istniejącej:

findTagByClass: aString
    ^ self findTagByClass: aString ifAbsent: [nil]


Oczywiście tworzymy ją w instancji klasy SoupTag w pakiecie Soup-Core. Widać w kodzie, że funkcja ta wywołuje jeszcze inną, z parametrem ifAbsent, która to jest główną (rozszerzoną funkcją) do wyciągania tagu. Jej nazwę też trzeba podmienić. W sumie tworzymy dwie funkcje:

findTagByID: aString
    ^ self findTagByID: aString ifAbsent: [nil]


i

findTagByID: aString ifAbsent: aBlock
    self findTag: [:aTag | (aTag attributeAt: 'id') = aString]
        ifPresent: [:aTag | ^ aTag].
    ^ aBlock value


Mając te dwie funkcje mogę już wyciągnąć tag, w którym siedzą komentarze:

root:= aSoup findTagByID: 'ctl00_phContentLeft_panUpdateComment'.

Można użyć "Print it", które wyświetli treść tagu, lub dać "Do it" i podejrzeć obiekt root przez zaznaczenie go i użycie "Inspect it".

Teraz czas wyciągnąć pojedyncze pola komentarzy. Tutaj będzie trochę trudniej, bo id komentarzy zmienia się. To jest, id komentarzy jest w postaci: komentarz_124234. Niestety, Soup nie umożliwia wyciągania tagów z użyciem wyrażeń regularnych. Trzeba będzie sobie poradzić. I znów trzeba zmodyfikować nasze nowe funkcje:


findAllTagsByIDregX: aString
    ^ Array streamContents: [ :aStream |
                self findAllTagsByIDregX: aString
                    stream: aStream ]

i

findAllTagsByIDregX: aString stream: aStream
    self childTagsDo:
        [ :aTag | ((aTag attributeAt: 'id') isNil ) ifFalse:
            [((aTag id) matchesRegex: aString) ifTrue: [aStream nextPut: aTag ]].
        aTag findAllTagsByID: aString stream: aStream ]

oraz:

findAllTagsByID: aString
   
^ Array streamContents:  [ :aStream |
               
self findAllTagsByID: aString stream: aStream ]i

findAllTagsByID: aString stream: aStream
    self childTagsDo:
        [ :
aTag | (aTag attributeAt: 'id') = aString
            ifTrue: [
aStream nextPut: aTag ].
       
aTag findAllTagsByID: aString stream: aStream ]

Mając te funkcje teraz mogę wyciągnąć komentarze do tablicy:


komentIDregX
:= 'komentarz_[0-9]+'.
komentSoups := root findAllTagsByIDregX: komentIDregX.


Super. ;) Teraz utworzę obiekty na tagi komentarzy, nicków i logo komentujących:


komentClass := 'text-h75 tresc'. 
imgClass := 'border small float-left'.
nickClass := 'text-h65 font-heading display-inl_blk nick'.
nixesTags := root findAllTagsByClass: nickClass.
imgsTags := root findAllTagsByClass: imgClass.
komentTags := root findAllTagsByClass: komentClass.

OK. Teraz wyciągamy tekst i inne ciekawe rzeczy:


nixes := OrderedCollection new.imgs := OrderedCollection new. 
koments := OrderedCollection new.nixesTags do: [:tag | nixes add: (tag text) ].
imgsTags do: [:tag | imgs add: (tag src) ].
komentTags do: [:tag | koments add: (tag text) ].


Oto rezultat:


Jak ktoś chce sprawdzić czy adresy obrazków są dobre, to może wyświetlić np. pierwszy poleceniem:

(ImageMorph fromStream: ((imgs at: 1) asUrl retrieveContents contentStream)) openInWorld.


Aby pozbyć się obrazka trzeba wcisnąć Shift i kliknąć na obrazku środkowym przyciskiem myszki.

Oczywiście poprzedni kod można zapisać krócej używając słownika i listy. Słownik (Dictionary) to odpowiednik mapy w Javie. Ostateczna postać:

zupa := Soup fromUrl: 'http://www.d***y.pl/aeroflyluby/Domowy-sposob-na-Diode,36474.html'.
root:= zupa findTagByID: 'ctl00_phContentLeft_panUpdateComment'.

komentIDregX := 'komentarz_[0-9]+'.
komentClass := 'text-h75 tresc'.
imgClass := 'border small float-left'.
nickClass := 'text-h65 font-heading display-inl_blk nick'.

komentSoups := root findAllTagsByIDregX: komentIDregX.

komenty := OrderedCollection new.
komentSoups do: [:ks |
      koment := Dictionary new.
        koment at: #nick put: (ks findTagByClass: nickClass) text.
        koment at: #img put: (ks findTagByClass: imgClass) src.
        koment at: #text put: (ks findTagByClass: komentClass) text.
        komenty add: koment.
    ].

(ImageMorph fromStream: (((komenty at: 1) at: #img) asUrl retrieveContents contentStream)) openInWorld.


Podsumowanie na video w jeszcze innym stylu:


Na filmie można zauważyć, że nie tworzę funkcji przed wykonaniem kodu. Środowisko Smalltalka informuje mnie, że takowej nie ma i pozwala utworzyć brakującą. Po utworzeniu funkcji można kliknąć "Proceed" i środowisko zachowa się jakby nic nie było. W ten sposób także bardzo łatwo i szybko pisze się testy w konwencji TDD - test first. To nie żadna nowość. Smalltalk był pierwszym szeroko dostępnym środowiskiem, które było wyposażone w bibliotekę do testów jednostkowych xUnit (jUnit - wersja na Javę). To, czym dziś ludzie się zachwycają i biorą za nowość, było używane grubo ponad 30 lat temu.

W kolejnej części o tym jak to wszystko wyświetlić na stronie WWW.

2012-09-22

Smalltalk: Seaside z użyciem Pharo



Chciałbym zaprezentować dwie rzeczy:

(c) Robert Tinney
  • i webowy framework zbudowany na jego podstawie: Seaside. W oparciu o nowoczesną, darmową implementację Pharo
Aby lepiej zrozumieć wpis najlepiej zacząć od ściągnięcia Seaside 3.0.7 w wersji One-Click. W katalogu Seaside.app, po rozpakowaniu będą skróty do uruchomienia na trzech systemach: Linux, Mac i Windows.


Krótko o Smalltaku

dla tych, co im się nie chce czytać Wiki + moje własne obserwacje:
 Smalltalk to dynamiczny język zorientowany obiektowo, zaprojektowany z myślą o zintegrowanym środowisku programowania. Smalltalk nie posiada struktury plików z jakich korzystają inne języki programowania. Wszystko żyje w obrazie, który jest obsługiwany przez maszynę wirtualną. Rozwiązanie bardzo podobne do systemu operacyjnego uruchomionego w Virtual Box. Daje to m. in. możliwość zapisania stanu w jakim jest w danej chwili - tzw. snapshot. Język jest tak prosty, że wdraża się go w OLPC jako pomoc edukacyjną dla dziesięciolatków.

Na dzień dzisiejszy popularne i darmowe odmiany Smalltalka : Pharo, Squeak (OLPC), GNU Smalltalk, działają tylko na jednym rdzeniu. To jest spuścizna po latach '80 i '90, gdzie nikt nie przejmował się skalowalnością horyzontalną na jednej maszynie, bo procesory były wtedy jednordzeniowe. Mogę powiedzieć, że i dziś nie jest to żadną wadą. System webowy w oparciu o Seaside tworzy się najczęściej w oparciu o Apache, który zajmuje się rozsyłaniem żądań do maszyn wirtualnych, które działają na różnych portach i różnych procesorach. W systemie Linux łatwo wskazać na jakim rdzeniu ma się uruchomić dany program. W przypadku awarii maszyny po prostu podnosi się kolejną na tym samym porcie. Wszystko to w zwykłym skrypcie powłoki systemowej.

Smalltalk to język, w którym nie wywołuje się funkcji tak jak w językach imperatywnych, a jedynie przekazuje wiadomości do obiektów (wysyła). Dość ciekawa sprawa, bo kompilator wysyła wiadomość do obiektu, a w momencie, gdy obiekt i klasy macierzyste nie obsługują komunikatu pokazuje się wyjątek 'doesNotUnderstand' ('nie rozumiem'). Nie jest to wyjątek taki jak w Javie, czy C, że program podnosi ręce do góry i często kończy działanie. Tutaj pokazuje się okienko z ostrzeżeniem i możliwością dodania obsługi komunikatu do obiektu. Po edycji dajemy restart i program działa dalej jakby nic. Ta wbrew pozorom błaha funkcja pozwala na pisanie testów w konwencji TDD - 'test first'.
Smalltalk posiada jedną z najczystszych składni językowych. Nie najkrótszą, a najczystszą. Jest bardzo zbliżony w wyglądzie do zwykłego angielskiego. 


myArray at: 1 put: 'tekst'.

Sentencję kończy się kropką (w C średnikiem). Komentarz umieszcza się w "podwójnym cudzysłowie". Łańcuchy znaków łączy się przecinkiem: napi := 'aaaa','bddd'.
W języku występują tzw. bloki. Są to kawałki kodu ujęte w nawiasy kwadratowe. Służą do przekazywania wykonania kodu w argumencie wiadomości lub mogą być zwracane. Posiadają także argumenty, np.:


elements := 5.
tab := Array new: elements.
1 to: elements do: [ :a | tab at: a put: (a * a)]

Jak to działa:
- utwórz obiekt SmallInteger (32 bit) z wartością 5
- utwórz tabelę tab jako wektor 5 obiektów (wszystkie mają wartość nil)
- dla każdego elementu w tablicy przypisz kwadrat wartości jego indeksu



Jak widać na powyższym obrazku obiekt można podejrzeć zaznaczając go i klikając w menu Inspect it. Okienko inspektora posiada na dole wygodne miejsce, w którym można wpisywać kod i np. w ten sposób modyfikować obiekt.


Przydatne skróty klawiszowe:
  • Ctrl + s, - (Accept) zapis zmian
  • Ctrl + d, - (Do it) wykonanie kodu
  • Ctrl + p, - (Print it) wykonanie kodu i pokazanie wyniku na końcu zaznaczonego bloku
  • Ctrl + i, - (Inspect it) - podgląd obiektu
Niektórzy zastanawiają się w tym momencie co to są te pola z dwukropkiem np. 'put:'.
Są to tzw. settery - funkcje, które odpowiadają za odebranie i przeróbkę obiektu podanego jako parametr. Setter obiektu może przyjąć tylko jeden parametr. Bez dwukropka to getter, czyli funkcja, która zwraca wartość i/lub wykonuje działanie na obiekcie. Zmienne instancji obiektu (są także zmienne klasowe - odpowiednik static w Javie) nie są widoczne z zewnątrz, stąd użycie getterów i setterów. Obiekty są dzielone w IDE na dwie strony: część instancji (instance) i część klasową (class). Strona instancji jest miejscem, gdzie przechowuje się zmienne i funkcje, które mogą się zmieniać z każdą kopią obiektu. Część klasowa jest wspólna dla wszystkich kopii, a także jest dostępna bez potrzeby tworzenia nowego obiektu.


Klasa w Smalltalku ma postać:

Object subclass: #MojaKlasa
    instanceVariableNames: 'zmienna_prywatna1 zmienna_prywatna_2'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Moja-Kategoria'


Zwykle setter ma postać:


zmienna_prywatna1: param
 zmienna_prywatna1 := param.

a getter:


zmienna_prywatna1
 ^ zmienna_prywatna1
 
Pierwsza linijka to jednocześnie nazwa funkcji i parametry. Zamiast pisać znane z C: 
typ getA(void) , pisze się tylko a . Zamiast pisać void setA(int a) pisze się a: aInt.

Znaczek ^ oznacza zwrócenie obiektu - słowo return z Javy/C. Średnika używa się do powtórzeń operacji na tym samym obiekcie. Zamiast pisać tak:


tablica_do_liczenia_sensu_zycia = Array new: 3.
tablica_do_liczenia_sensu_zycia at: 1 put: 'a'.
tablica_do_liczenia_sensu_zycia at: 2 put: 'y'.
tablica_do_liczenia_sensu_zycia at: 3 put: 'z'.

można pisać tak:


tablica_do_liczenia_sensu_zycia at: 1 put: 'a';
                                at: 2 put: 'y';
                                at: 3 put: 'z'.

Każdy obiekt można edytować. I to w sposób jaki jest to nieosiągalny dla innych języków. Mam tu na myśli przede wszystkim popularną rodzinę języków C. Przykładowo mogę dodać brakującą funkcjonalność do klasy String - nie do pomyślenia w Javie. Jako, że mamy do czynienia tylko z obiektami i przesyłaniem komunikatów w tym języku nie ma operatorów. Przykład:


wynik_działania := 1 + 2 * 3.
wynik_działania -> 9

Działa to w ten sposób: do obiektu '1' prześlij komunikat '+' i obiekt '2'. W tym momencie obiekt '1' zwraca obiekt : '3' do którego przesyłany jest komunikat '*' i obiekt '3'.
Aby dostać prawidłowy wynik trzeba iloczyn wziąć w nawias. Kod w nawiasach jest wykonywany jako pierwszy. 




Podstawowe obiekty liczbowe

Podstawowym obiektem, z którego wywodzą się inne (poprzez dziedziczenie) jest obiekt Number. Zawiera on podstawową obsługę wiadomości typu '+' '-' '/' 'negated' 'abs', 'even', 'odd',konwersję typów, testowanie,  itp.

Kolejnym obiektem jest typ całkowity Integer. Nie jest jednak bezpośrednio używany, a posłużył do utworzenia dwóch klas pochodnych: SmallInteger i LargeInteger. SmallInteger posiada rozmiar 32-bit z wartościami od -2^30 do 2^30-1. LargeInteger przejmuje na siebie odpowiedzialność w przypadku sytuacji gdy jego wcześniejszy kolega próbuje 'przekręcić licznik'. LargeInteger jest ograniczony ilością dostępnej pamięci. Przykład:

a := 400000000  "-> SmallInteger"
a := a * 400000000 "-> zamiana typów"
a -> 80000000000000000 "-> LargeInteger"

Kolejny typ to typ ułamkowy Fraction. Powstaje gdy w czasie dzielenia biorą udział wartości całkowite. Użycie typu ułamkowego nie powoduje utraty precyzji związanej z zaokrągleniem.

a := 1 / 2
a -> (1/2)

a asFloat
a -> 0.5

Float to typ zmiennoprzecinkowy zgodny ze standardem  IEEE-754 . Zakres do +/- 10^307
Więcej  można zobaczyć przeglądając paczkę Kernel-Numbers.


Instrukcje warunkowe i pętle


W Smalltalku nie ma instrukcji warunkowych i pętli. Są tylko komunikaty i operacje na blokach. Np. znana z C instrukcja: 

if ( a > 0 ) a = a * 5; 

w Smalltalku przyjmuje postać: 

( a > 0 ) ifTrue: [ a := a * 5 ].  

- do zmiennej a przesyłana jest wiadomość '>' z parametrem 0. Obiekt a następnie wysyła nowy obiekt Boolean z ustawioną wartością true lub false. Do tego obiektu wysyłana jest wiadomość ifTrue: z blokiem [ a := a * 5 ] jako parametr. Jeżeli warunek jest spełniony obiekt Boolean wykona blok.
Pętle są realizowane w podobny sposób, np.: 
[ a > 0 ] whileTrue: [ a := a - 1 ].

 Tym razem to blok [ a > 0 ] otrzymuje komunikat whileTrue: z blokiem 
 [ a := a - 1 ] jako parametr. Trochę więcej o blokach:


Bloki

 

Blokiem jest kod zapakowany w nawiasy kwadratowe. Np.:


[ 1 + 2 ]

Blok zawsze zwraca sam siebie, chyba, że poprosimy o wartość (value):


blok := [ 1 + 2 ].
blok -> [ clojure ] in UndefinedObject
blok value. -> 3

Blok może przyjmować parametry:


sqrt := [ :a | ( a >= 0) ifTrue: [a sqrt]
                         ifFalse: [Complex new real: 0 imaginary: (a abs sqrt)]].

sqrt value: 4. -> 2
sqrt value: -4. -> 0 + 2i 

suma2arg := [:a :b | a + b ].
suma2arg value: 1 value: 2. -> 3

[:a :b | a + b ] value: 1 value: 2. -> 3

Kolejny szczegół: obiekt może mieć tylko jedną klasę bazową. Nie ma wielokrotnego dziedziczenia. Interpreter i kompilator Smalltaka są napisane w Smalltalku. Pozwala to uruchomić maszynę wirtualną wewnątrz maszyny wirtualnej. To tak jakby się dało uruchomić Javę w Javie.




Praca w Smalltalku jest pracą z całym systemem. Nie ma tu podziałów na programy. Programem, jest wynik modyfikacji systemu. 

Smalltalk nie potrzebuje systemu operacyjnego by działać. Są wersje edukacyjne dla dzieci, które bootują do Smalltalka. Jako ciekawostkę podam, że w innych, bardziej rozwiniętych krajach naukę programowania dla dzieci zaczyna się od Smalltalka. U nas zaczyna się od Logo. Nie wiem czyj to wymysł, ale znając ten kraj można się było tego spodziewać.

Tworzenie nowej klasy w Smalltalku to wysłanie komunikatu do obiektu, po którym chcemy dziedziczyć lub do podstawowego obiektu 'Object'. Nie tworzy się żadnego pliku tak jak w Javie. Oczywiście utworzony obiekt można zapisać do pliku, np w celu archiwizacji.
I ostatnia dość ciekawa cecha środowiska, której nie spotkałem w żadnym języku, czy IDE, czyli: wykonywanie kodu gdziekolwiek i kiedykolwiek. To cecha, która pozwala bardzo szybko testować kod. Słabą namiastką tego są tak zwane REPL w innych językach, czyli interaktywne konsole, gdzie kawałek kodu się kopiuje bądź wysyła i on tam jest wykonywany. Przykład: (Transcript to odpowiednik stdout).





Gdy chcę zobaczyć wynik operacji w okienku, którym piszę, używam opcji 'Print it'. Np.:

 

Teraz trochę o Seaside

Małe intro na początek:

Widać trochę machania myszką, ale to efekt usunięcia ścieżki dźwiękowej, która nie nadawała się do publikacji. 


Seaside jest oparty o model komponentowy. Obiektowość Smalltalka bardzo ułatwia ich tworzenie i edycję. Seaside pozbywa się także lub stara się pozbyć instrukcji GOTO. Znanej i pogardzanej instrukcji, która została wypędzona w programowaniu klasycznym dzięki licznym egzorcyzmom, a która w programowaniu webowym manifestuje się w tagu HREF. W Seaside kliknięcie w odnośnik na stronie powoduje wysłanie wiadomości do obiektu, który decyduje jak odpowiedzieć. Seaside nie wykorzystuje szablonów. Tutaj wszystko tworzy się w jednym języku. Chodzi o to, by jak najbardziej zbliżyć się w stylu tworzenia do aplikacji desktopowych.

Seaside posiada wbudowane i przezroczyste wsparcie dla jQuery i jQuery UI. Ajax jest bardziej bezbolesny niż w Ruby on Rails. Tutaj w ogóle można nie dotykać JavaScriptu.
Ogólne podejście z tego, co zauważyłem zamyka się w tworzeniu rdzenia aplikacji w Seaside, a reszta treści: multimedia, css i inny statyczny kontent serwuje się oddzielnie. Oczywiście można wciągnąć CSS i obrazki do obrazu aplikacji, ale o ile nie jest to pojedyncza aplikacja, to będzie tylko zajmować niepotrzebnie RAM.
W Seaside dzięki użyciu kontynuacji użycie przycisku przeglądarki 'Wstecz' i 'Następny' nie powoduje zaburzenia w działaniu aplikacji. 

Jako ciekawostkę dodam, że jest już maszyna wirtualna, która działa na Androidzie. Działa, bo sprawdziłem. ;) Polecam tablet 10 cali, bo będzie ciężko z rozdzielczością. Najlepiej ściągnąć i zainstalować samą maszynę wirtualną, a później skopiować plik .image i .changes na kartę SD. Aplikacja sama znajdzie pliki i wyświetli listę, z której można uruchomić obraz Seaside.

To rozwiązanie z Androidem jest o tyle dobre, że ten sam obraz można uruchomić na wielu systemach bez kompilacji. Nawet Java i inne języki tak nie potrafią. Przede wszystkim to zaleta Open Source, które to dzięki dostępności kodu źródłowego pozwoliło skompilować maszynę na Androida. BTW. Ktoś czeka aż Java od Oracle pojawi się na Androidzie? ;)