"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-11-22

Clojure Elementary 2




Jest wieczorek i jest chwilka czasu, więc kontynuuję to, co zacząłem w poprzednim wpisie.

 Problem 9: Intro to Maps


Mapa (inaczej zwana hash-map) to słownikowa struktura danych, która przechowuje dwa uporządkowane rodzaje danych, to jest: klucz, który posiada unikalną wartość w całej mapie i odpowiadającą mu wartość. Zarówno klucz jak i przypisane do niego dane mogą być dowolnego typu. Mapa jest strukturą nieuporządkowaną i nie zachowuje kolejności par klucz-dane. Podobnie jak w strukturze Set. Mapę można traktować jako funkcję, która bierze za parametr klucz i zwraca wartość przypisaną do klucza. Jeżeli użyję klucza Clojure (słowo zaczynające się od dwukropka) jako funkcji i podam mu mapę jako parametr, wyrażenie zwróci wartość pod kluczem w mapie. Jeżeli klucz nie istnieje wyrażenie w obu przypadkach zwraca nil. Mapę można odróżnić po okrągłych nawiasach klamrowych.
Zadanie polega na odgadnięciu wyniku podania mapie klucza jako parametr i kluczowi mapy.
T1: (= __ ((hash-map :a 10, :b 20, :c 30) :b))
T2: (= __ (:b {:a 10, :b 20, :c 30}))
E1: 20
E2: ({1 20} 1)
E3: (:k {:k 20})

Problem 10: Maps: conj


Działanie funkcji conj na zbiorze typu mapa. W przypadku mapy funkcja conj bierze za pierwszy argument mapę, a kolejne argumenty dane ułożone w pary, np. dwuelementowy wektor lub mapę. Nie da się zapodać Seta jako parametr ze względu na niewiadomą kolejność jego elementów.

T1: (= {:a 1, :b 2, :c 3} (conj {:a 1} __ [:c 3]))
E1: {:b 2}
E2: [:b 2]
E3: (hash-map :b 2)

Problem 11: Intro to Sequences


Sekwencja w Clojure to uogólniona struktura danych, która może być listą, wektorem lub mapą. W związku z tym, że Clojure to Lisp wymagane jest by można było się poruszać po elementach struktur danych z użyciem funkcji first, last, second itd, które operują na listach. Chodzi o to by nie rozdrabniać się i tworzyć różnych funkcji dla wektorów, list i map.

Zadanie polega na odgadnięciu wyniku działania poszczególnych funkcji testowych. Funkcja first zwraca pierwszy element ciągu, second drugi, last ostatni.

T1: (= __ (first '(3 2 1)))
T2: (= __ (second [2 3 4]))
T3: (= __ (last (list 1 2 3)))

E1: 3
E2: (first '(3))
E3: (last '(3))
E4: (second (first (list '(1 3) 2)))

Problem 12: Sequences: rest


Zadanie polega na odgadnięciu wyniku działania funkcji rest. Funkcja rest zwraca wszystkie elementy ciągu poza pierwszym

T1: (= __ (rest [10 20 30 40]))

E1: [20 30 40]
E2: '(20 30 40)

Problem 13: Intro to functions


W Clojure istnieje wiele sposobów by zdefiniować funkcję:

1. Za pomocą funkcji fn. Dzięki niej tworzymy funkcje anonimowe. Nazwa za fn to nazwa funkcji. Nie daje ona nic oprócz tego, że łatwiej rozpoznać miejsce błędu w przypadku gdy program się posypie.

2. Inny sposób tworzenia funkcji anonimowych: za pomocą znaku number. np.: #(+ % 1). % - jest parametrem przekazanym do funkcji. Jeżeli do tej funkcji przekazywane jest więcej niż jeden argument, to można użyć znaku % z numerem. np. : %1 , %2 itd..

3. Funcja partial służy do uzupełniania parametrów funkcji, która jest jej pierwszym argumentem. W ten sposób można sobie skrócić kod, gdy wartość jednego z argumentów funkcji jest znana. W tym: T4 wypadku przekazywany jest dodatkowy argument do funkcji '+'.

Aby zapisać funkcję pod daną nazwą można użyć makra def do którego przekazujemy nazwę i funkcję anonimową, lub uproszczonego defn.

Tym razem trzeba zgadnąć wynik jaki ma pojawić się po przekazaniu parametru do funkcji anonimowej.

T1: (= __ ((fn add-five [x] (+ x 5)) 3))
T2: (= __ ((fn [x] (+ x 5)) 3))
T3: (= __ (#(+ % 5) 3))
T4: (= __ ((partial + 5) 3))

E1: 8

Problem 14: Hello World


Czas na Hello World. Można zapytać. czemu tak późno? Temu, że to specjalny Hello world. W tym miejscu powoli kończą się nasze zgadywanki, a zaczyna praca.
Zadanie polega na utworzeniu funkcji anonimowej, która weźmie za parametr podany ciąg znaków i doda do niego Hello z !. Jako, że z poprzedniego zadania wiemy już jak tworzyć funkcje anonimowe zadanie jest trywialne. ;) .

T1: (= (__ "Dave") "Hello, Dave!")
T2: (= (__ "Jenn") "Hello, Jenn!")
T3: (= (__ "Rhea") "Hello, Rhea!")

E1: #(str "Hello, " % "!")
E2: (fn [arg] (str "Hello," arg "!"))
E3: (fn hello-hi [arg] (str "Hello," arg "!"))
E4: #((partial str "Hello, ") % "!")

Problem 15: Double Down


Podwójny daun. Zadanie polega na zgadnięciu postaci funkcji, która w magiczny sposób podwoi wartość jej argumentu.

T1: (= (__ 2) 4)
T2: (= (__ 3) 6)
T3: (= (__ 11) 22)
T4: (= (__ 7) 14)

E1: #(* % 2)
E2: (fn [d] (* d 2))

Problem 16: Sequences: map


Kolejna zgadywanka. Uwaga, by nie mylić funkcji map z mapą (hash-map). Funkcja map służy do zwracania leniwej kolekcji (leniwej, bo wartość jej elementów liczona jest dopiero gdy są potrzebne - o tym później). Funkcja ta bierze za argument funkcję i minimum jedną niepustą kolekcję elementów (wektor, lista, mapa), na których kolumnami, po kolei wykonuje podaną funkcję. Przykładowo:
(map + [1 2 3]) daje: [1 2 3], bo (+ 1) daje 1
(map + [1 2 3] [4 5 6]) daje : [5 7 9], bo (+ 1 4) to 5, (+ 2 5) to 7...

Działanie jest wykonywane kolumnami. Jako, że funkcja + może wziąć dowolną liczbę parametrów można do map dawać dowolną ilość kolekcji. Jeżeli kolekcje mają różne długości. Map wykona działanie na ilości elementów równej długości najkrótszej kolekcji.
Może narysuję o co chodzi:

(map + '(1 2 3) '(4 5 6) '(1 1 1)) to:
         
         1 | 2 | 3
         4 | 5 | 6
         1 | 1 | 1
       + ---------
         6 | 8 |10

Wynik:
         '(6 8 10)

(map + '(1 2) '(4 5 6) '(1 1 1)) to:
         
         1 | 2
         4 | 5
         1 | 1
       + -----
         6 | 8

Wynik:
       '(6 8) - 2 elementy, bo najkrótsza lista miała długość 2

(map #(+ % 5) '(1 2 3) '(4)) to:
ArityException Wrong number of args (2) passed to: core$eval1442$fn
  clojure.lang.AFn.throwArity (AFn.java:437)

Dlaczego błąd? Bo w przeciwieństwie do funkcji + funkcja anonimowa
#(+ % 5) przyjmuje tylko jeden parametr, a wiemy, że map przekazuje
parametry kolumnami. W tym przypadku map wysłał do funkcji 1 i 4

Aby wyrażenie było prawidłowe należy uwzględnić drugi parametr
w funkcji anonimowej: #(+ %1 %2 5)
(map #(+ %1 %2 5) '(1 2 3) '(4)) to: (10), bo 1 + 4 + 5 = 10
- reguła o obcinaniu list wciąż obowiązuje.

I zadanie:

T1: (= __ (map #(+ % 5) '(1 2 3)))

E1: '(6 7 8)
E2: [6 7 8]

Clojure Elementary 1

Tak sobie siedzę i myślę, że można by tu coś napisać dla potomności. W szczególności dla początkujących, a i osobiście samemu podciągnąć się z języka.



Twórca Clojure : Rich Hickey
Postanowiłem więc, że napiszę wstępniaka wzorując się na problemach ze strony 4Clojure . Strona zawiera zestaw problemów podzielonych na kategorie o różnych stopniach trudności. Postaram się w każdym nowym wpisie na blogu rozwiązać kilka z nich. Oczywiście zacznę od najłatwiejszych. Pomysł poznawania Clojure dzięki przykładom, które mogą być testowane jest wyjątkowo ciekawy. Nie spotkałem jeszcze czegoś podobnego dla innych języków. Nie ma jednak tak wspaniale jakby się można było spodziewać. Często zadania z pozoru trywialne rozwiązuje się dość długo ze względu na zbyt uproszczoną dokumentację. Aby nie tracić czasu i nie zanudzać może od razu przejdę do rzeczy.

Problem 1: Nothing but the truth

Zadanie polega na podaniu w miejscu : ___ wyrażenia, które redukuje się do postaci prawda/fałsz. Może to być wyrażenie logiczne lub po prostu : true. Zadania będę oznaczać literą T i numerem, a przykładowe odpowiedzi literą E i numerem. Odpowiedzi można wpisywać na stronie w polu tekstowym i kliknąć w przycisk Run. Jeżeli zadanie zostanie rozwiązane poprawnie, to przy każdym problemie zapali się zielona dioda. Jeżeli nie, zapali się czerwona. Do testów polecam tę stronę: try Clojure. Do bardziej zaawansowanych rzeczy będzie potrzebny prawdziwy Clojure z IDE. Polecam także ściągawkę, która posiada wiele przykładów.

Zaczynamy:
T1: (= ___ true)
Funkcje zwracające prawdę lub fałsz w Clojure to : <, >, =, not, not=, >=, <= oraz wszelkie funkcje kończące się znakiem: ? . Pytajnik nie ma wpływu na działanie funkcji, to część jej nazwy. Po prostu taka zasada została przyjęta w nazewnictwie funkcji, które zwracają wartości logiczne. nil to odpowiednik znanego null z innych języków i oznacza wartość nieokreśloną.
E1: true
E2: (= 1 1)
E3: (not false)
E4: (>= 2 1)
E5: (>= 2 2)
E6: (not= 2 3)
E7: (< 1 2 3 4)
E8: (> 4 3 2 1)
E9: (nil? nil)

Problem 2: Simple math

Zadanie polega na odgadnięciu liczby, bądź wyrażenia, które spełni warunek:
T1: (= (- 10 (* 2 3)) __)
Rozkładając wyrażenie (- 10 (* 2 3)) na postać znaną ze szkoły mam wyrażenie:
10 - (2 * 3)
którego wynik to: 4, stąd, możliwe odpowiedzi:
E1: 4
E2: (* 2 2)
E3: (+ 2 2)
E4: (/ 8 2)
To: (/ 4 1.0) wyrażenie mimo, że wydaje się, że poprawne zwraca fałsz. Co się dzieje? Otóż wynikiem tego działania nie jest 4 a 4.0 - różny typ danych. Pierwszy jest wartością całkowitą, drugi przybliżoną - tzw. zmiennoprzecinkową. Reprezentacje bitowe tych liczb są różne. Aby równanie było prawdziwe test musiałby wyglądać tak: (== ... ), czyli zawierać funkcję ==, która porównuje wartości bez zwracania uwagi na typ danych wykonując najpierw konwersję, a potem porównanie.

Problem 3: Intro to Strings

Teraz musimy zgadnąć co zwróci funkcja: .toUpperCase. Oczywiście funkcja zamienia w tekście małe znaki na duże.
T1: (= __ (.toUpperCase "hello world"))
E1: "HELLO WORLD"
E2: (str "HELLO" " " "WORLD")
E3: (str \H \E \L \L \O \  \W \O \R \L \D)
str = funkcja, która łączy wszystkie argumenty w jeden ciąg znaków. Można zapodać liczbę, symbol, znak : \A. Typ String w Clojure jest tym samym typem z Javy przeniesionym bez zmian. Działają więc na nim wszystkie funkcje z Javy. Funkcję Javy można łatwo rozpoznać po kropce na początku nazwy.

Problem 4: Intro to Lists

Zadanie polega na przekazaniu odpowiednich argumentów do funkcji list.
Listę można utworzyć za pomocą polecenia list lub ujmując dane w: '( ) - nawiasy przed którymi jest apostrof. Słowa zaczynające się dwukropkiem nazywane są kluczami. Klucze to jeden z podstawowych typów danych, który ewaluuje się na siebie, tak jak liczba.
T1: (= (list __) '(:a :b :c))
E1: :a :b :c

Problem 5: Lists: conj

Funkcja conj. Wywołuje się ją podając pierwszy argument jako listę/wektor lub set, a resztę już jako dowolny typ danych. Zwraca ona nową listę/wektor/set z dodanymi polami do podanej struktury. Conj nowe elementy listy umieszcza na jej początku, a do wektora łączy na końcu. W przypadku seta nie ma znaczenia. Set jest strukturą o nieuporządkowanej kolejności elementów. Funkcja zwróci wyjątek się jeżeli pierwszy argument nie będzie listą lub wektorem/setem i nie będzie minimum dwóch argumentów. Co ciekawe, funkcja porównawcza nie zaprotestuje jeżeli zapodać jej listę i wektor. Pod względem działania z zewnątrz wektor jest równoważny liście, pod spodem działa trochę inaczej.
T1: (= __ (conj '(2 3 4) 1))
T2: (= __ (conj '(3 4) 2 1))
E1: '(1 2 3 4)
E1: (list 1 2 3 4)
E3: [ 1 2 3 4 ]
E4: (vector 1 2 3 4)
E5: (vec '(1 2 3 4))

Problem 6: Intro to Vectors

Jak wspominałem wcześniej wektory mogą być porównywane bezpośrednio z listami.
Zadanie polega na wypisaniu elementów wektora.
T1: (= [__] (list :a :b :c) (vec '(:a :b :c)) (vector :a :b :c))
E1: :a :b :c
Funkcja vector tworzy wektor z argumentów. Funkcja vec tworzy wektor z listy.

Problem 7: Intro to Sets

Set, to lista, w której mogą znajdować się tylko unikalne wartości w losowej kolejności. W związku z tym nigdy nie należy brać pod uwagę rozmieszczenia jego elementów. Dla rozróżnienia oznacza się go okrągłymi klamrami ze znaczkiem number przed pierwszą klamrą: #{ }. Zadanie polega na odgadnięciu wyniku. Set tworzy się za pomocą funkcji set z listy lub wektora. Funkcja union łączy dwa sety w jeden. Kolejność elementów w secie nie ma znaczenia.
T1: (= __ (set '(:a :a :b :c :c :c :c :d :d)))
T2: (= __ (clojure.set/union #{:a :b :c} #{:b :c :d}))
E1: #{:a :b :c :d}
E2: #{:b :a :d :c}
E3: (clojure.set/union #{:a :b} #{:c} #{:d})

Problem 8: Sets: conj

Funkcja conj w zastosowaniu do setów.
T1: (= #{1 2 3 4} (conj #{1 4 3} __))
E1: 2

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? ;)