Clojure @ JUG

Transkrypt

Clojure @ JUG
Clojure
Obca technologia wśród nas
Filip Koczorowski
luty 2010
Historia tej prezentacji zaczyna się kilka miesięcy temu. Rozmawiałem z
Adamem Dudczakiem w październiku 2009 roku o tym, że ewentualnie
mógłbym poprowadzić prelekcję na JUG. Z kilku moich propozycji tematów
najbardziej zainteresował go ten o Clojure. Skomentował go słowami "to
byłoby fajne, programowanie funkcjonalne to teraz gorący temat".
Grzecznie pokiwałem głową, chociaż później stwierdziłem, że to nie do
końca jest tak.
Co to jest Clojure?
funkcyjny
dynamiczny
działający na JVM
również na CLR
Rzeczywiście, Clojure jest wręcz sztandarowym przykładem języka ze
zdobywającego ostatnio popularność nurtu programowania funkcyjnego.
Używa silnej i dynamicznej kontroli typów, co również jest popularnym
motywem.
To wszystko oferuje będąc językiem działającym na wirtualnej maszynie
Javy. Posiada daleko posuniętą dwukierunkową integrację z kodem
pisanym w języku Java i jest zgodny z tak napisanym kodem na poziomie
Java bajtkodu.
Jest jednak coś jeszcze, nad czym nie sposób przejść tak po prostu do
porządku dziennego...
Co to jest Clojure?
język
należący
do rodziny
Lisp
Clojure należy mianowicie do rodziny języków Lisp (dokładniej jest oparty
na Lisp-1, co czyni go trochę podobnym do języka Scheme).
Z faktu bycia Lisp'em wywodzi się wrażenie obcości Clojure, zupełnie jakby
nie pochodził z naszej planety. Różni się on znacząco od popularnych
języków takich jak C, Java, Python, Basic itd. Zaakceptowanie jego
odmienności może być trudne, ale jednocześnie bardzo satysfakcjonujące i
opłacalne.
Więc czym jest Clojure?
Można więc scharakteryzować Clojure jako dynamiczny język
programowania ogólnego przeznaczenia, działający na platformie JVM.
Jest to na dodatek język funkcyjny z niemodyfikowalnymi danymi. O tym co
to oznacza będzie za chwilę.
Jest to wreszcie jeden z dialektów Lisp i od tego zaczniemy.
Lisp - żywy dinozaur
wynaleziony w 1958 r.
Język Lisp wynalazł prof. John McCarthy z MIT ponad pół wieku temu (10
lat przed tym jak człowiek stanął na Księżycu).
Zdjęcie przedstawia autentyczną maszynę - IBM 704 z 1954r. na której
pierwsza wersja Lisp została uruchomiona.
W kategoriach informatycznych tak odległe czasy to chwila po Wielkim
Wybuchu. A może to właśnie wynalezienie Lisp spowodowało Wielki
Wybuch?
Lisp miał to wcześniej
instrukcję warunkową
funkcje jako wartości
dynamiczną kontrolę typów
rekurencję
odśmiecanie pamięci
program jako modyfikowalną
strukturę drzewiastą w pamięci
Wspomniany Wielki Wybuch uznawany jest za początek wszystkiego.
Podobnie Lisp. Oto niektóre, najważniejsze innowacje, które właśnie w
Lisp pojawiły się jako pierwsze. Część z nich może znacie ze innych
języków programowania.
Liczba tych innowacji jest naprawdę bardzo długa i co ciekawe, ostatnia
pozycja z tej listy ciągle nie została w pełni zaadoptowana w innych
językach.
Lisp potrafi się dostosować
początkowy brak OOP → własna
innowacyjna implementacja OOP
oparta o tzw. multimetody
Warto też zauważyć, że Lisp nie tylko wprowadzał, ale także chłonął
pomysły. Słynna rewolucja programowania zorientowanego obiektowo
(Object Oriented Programming) przyszła do rodziny języków Lisp trochę
później niż do innych. Wyjątkowo dobrze zintegrowana została jednak z
samym językiem i obecnie stanowi jeden z podstawowych sposobów
implementacji modelu OOP w języku programowania.
Lisp - cechy
wiele odmian
LISt Processing
wyrażenia symboliczne
tekstowa reprezentacja listy (struktury danych)
ang. symbolic expression
w skrócie sexp :)
O całej rodzinie języków Lisp można powiedzieć dwie rzeczy: jest ich
wiele, a wszystkie łączy posiadanie tzw. wyrażeń symbolicznych. Te
skrótowo nazywane s-wyrażenia, lub w oryginale s-expressions, to
tekstowa reprezentacja strukturyzowanych danych w postaci listy. Stąd
pochodzi nazwa Lisp - List Processing.
Wyrażenia symboliczne
(length (list 1 2 3 4))
Skąd pomysł, żeby wszystko traktować jako listę? Powody były bardzo
ważne i tajemnicze, a szczegóły giną już w mroku dziejów...
Tak naprawdę to znaczy, że nie wiem skąd pomysł się wziął, ale ma
istotną konsekwencję: kod i dane mają taką samą reprezentację w języku
Lisp. I oczywiście reprezentacją tą jest lista.
Wyrażenia symboliczne
(length (list 1 2 3 4))
instrukcja
dane
Skoro jednak kod i dane to to samo, jak odróżnić jedno od drugiego?
Stosowana jest prosta zasada - pierwszy element listy traktowany jest jako
instrukcja do wykonania.
Wyrażenia symboliczne
(length (list 1 2 3 4))
instrukcja
dane
Skoro jednak kod i dane to to samo, jak odróżnić jedno od drugiego?
Stosowana jest prosta zasada - pierwszy element listy traktowany jest jako
instrukcja do wykonania.
S-wyrażenia
<property
name="build"
location="build/classes" />
<mkdir dir="${build}" />
Jeśli dla kogoś pomysł z unifikacją kodu i danych wydaje się zbyt
abstrakcyjny, warto sięgnąć po bardziej znajomy przykład.
Apache Ant. Bardzo użyteczne narzędzie. Instrukcje Ant mają postać pliku
XML. Sam XML z kolei uznawany jest za format zapisu danych. I
rzeczywiście, w skrypcie Ant typowe instrukcje mieszają się z typowymi
danymi, a zapis jednych od drugich wiele się nie różni.
S-wyrażenia
<property
name="build"
location="build/classes" />
(def build "build/classes")
<mkdir dir="${build}" />
(mkdir build)
Analogicznie zapisany skrypt Lisp wyglądałby tak. Nie różni się specjalnie
od XMLowego zapisu, który stosuje Ant.
W praktyce jednak, Ant nie idzie tak daleko jak Lisp w wykorzystaniu faktu,
że instrukcje i dane mają tą samą reprezentację. Lisp pozwala bowiem
programom operować na kodzie programów. Stąd popularne określenie
Lisp jako "programowalnego języka programowania".
S-wyrażenia
<property
name="build"
location="build/classes" />
(def build "build/classes")
<mkdir dir="${build}" />
(mkdir build)
Na tym slajdzie wyróżniłem wyjątkowo dla Lisp charakterystyczne nawiasy.
To nie przypadek, że faktycznie każdy program w Lisp je zawiera.
Dlaczego?
Lisp - składnia
()
Bo stanowią jedyny stały element składni języka Lisp.
Nawiasem mówiąc, normalnie jestem przeciwny zamęczaniu wyliczaniem
wszystkich elementów składniowych języka na prezentacjach, ale tu
zrobiłem wyjątek.
REPL
Read
Evaluate
Print
Loop
R
E
P
Każdy element s-wyrażenia jest obliczany. Lisp działa etapowo, jak silnik
trójsuwowy. Pierwszy suw to wczytanie (Read). Wczytany tekst jest
obliczany w drugim suwie (Evaluate). W trzecim suwie wyliczona wartość
jest zwracana jako tekst, np. do wyświetlenia (Print). I cały cykl pracy jest
powtarzany (Loop).
Ten tryb działania pozwala na interaktywną pracę z programem, czyli coś
co oferują np. Ruby ze swoim IRB, Python, Scala itd. Tu ciekawostka - w
wielu przypadkach takie interaktywne konsole w innych językach są
nazywane "REPL".
Kluczową rzeczą jest tutaj fakt, że Read i Print operują na takim samym
formacie czystego tekstu, który może, ale wcale nie musi pochodzić z pliku
z kodem źródłowym.
REPL
Read
Evaluate
1. macro expand
2. evaluate
Print
Loop
Tak naprawdę to suw pracy (Evaluate) składa się z dwóch etapów. Przed
właściwym obliczeniem wartości występuje etap rozwinięcia makr.
Makra
tylko z pozoru takie jak w C
ten sam język co właściwy kod
kod piszący kod (który pisze kod,
który pisze...)
rozszerzalność języka
dynamiczność języka
Makra w Lisp to niesłychanie potężne narzędzie. Niestety nazywają się tak
samo, jak mechanizm stosowany w języku C. Tymczasem makro
z Lisp ma się do makra z C tak, jak krzesło do krzesła elektrycznego.
Lisp jako jedyny ma mechanizm makr, który operuje na tym samym
językiem i konstrukcjami składniowymi co reszta kodu. W praktyce
pozwala to na niespotykaną nigdzie indziej dynamiczność języka i niemal
nieskończoną rozszerzalność (patrz wcześniejszy slajd "Lisp potrafi się
dostosować").
Właśnie ta funkcjonalność Lisp pozwoliła mu na przetrwanie od lat 60.
ubiegłego wieku aż do dzisiaj.
Makra
Lisp
Java 5
możliwość
dodania własnej
konstrukcji pętli
możliwość dodania
dowolnej innej
konstrukcji
językowej
dodana nowa,
wygodna pętla
"for"
???
Makra to temat rzeka. Nie chcę przeciągać tej prezentacji w
nieskończoność, podam więc jeden przykład użyteczności mechanizmu
makr.
W składni języka Java w wersji 5 pojawiła się bardzo długo wyczekiwana i
wygodna w użyciu pętla "for" do iterowania po elementach kolekcji. Miliony
programistów odetchnęło z ulgą. Nie musieliby jednak na nią czekać,
gdyby ich język programowania pozwalał na tworzenie nowych konstrukcji
językowych, tak jak w Lisp.
I wreszcie Clojure
wszystko co wcześniej plus
trochę więcej składni
orientacja na funkcje
działanie na JVM
ułatwienia dla aplikacji wielowątkowych
Prezentacja miała dotyczyć języka Clojure, więc czas przejść do Clojure.
Właściwie wszystko to, o czym mówiłem do tej pory ma przełożenie na
Clojure, bo jest to Lisp. Dodatkowe cechy wymieniłem tutaj i zaraz je
omówię.
Clojure - składnia
("typowa dla Lisp" "lista")
["wektor" "czyli" "jak tablica"]
{:tablica "asocjacyjna"}
#{"zbiór" "różnych" "elementów"}
Obowiązkową dla Lisp składnię dla list Clojure rozszerza o wygodną
składnię definiowania wektorów (czyli z grubsza tradycyjnych tablic), tablic
asocjacyjnych (zwanych czasami mapowaniami) i zbiorów.
Odpowiedniki tych typów w języku Java to LinkedList, ArrayList, Map, Set.
Clojure - składnia
\tab
"String"
; komentarz do końca linii
#"wyrażenie regularne"
A oto kilka kolejnych elementów składniowych języka Clojure.
Clojure - składnia
\tab
(char \uNR_W_UNICODE)
"String"
(str \S \t \r \i \n \g)
; komentarz do końca linii
(comment "jakiś tekst")
#"wyrażenie regularne"
(re-pattern "wyrażenie regularne")
Pokazane tu elementy składniowe nie są w tym momencie jednak istotne
same w sobie. Co jest ważne, to fakt, że każdy z pokazanych tu
elementów, za pomocą tzw. makra czytającego, uruchamianego podczas
suwu READ w REPL, jest transformowany do standardowego s-wyrażenia.
Clojure - funkcyjny
funkcje jako:
wartości i kolejny typ danych
element zwracany z innych funkcji i
przekazywany jako parametr
niezmienne (niemodyfikowalne)
struktury danych
Clojure to tzw. język funkcyjny, albo inaczej "zorientowany na funkcje". W
praktyce wynikają z tego dwie podstawowe cechy.
Po pierwsze, funkcje są pełnoprawnymi danymi, które można zwracać jako
wynik działania innych funkcji, albo przekazywać jako parametry
wywołania.
Po drugie, wszystkie struktury danych w Clojure są niezmienne. To tak,
jakby np. w języku Java każda zmienna była "final".
Clojure - na JavaVM
kompilowany do bajtkodu
"lukier składniowy"
(import '(javax.swing JFrame JButton))
(let [frame (JFrame. "Test!")
do-button (JButton. "Start")]
(doto frame
(.add do-button)
(.setSize 200 60)
(.setVisible true)))
Clojure jest też pełnoprawnym językiem platformy Java Virtual Machine. A
to dlatego, że programy napisane w Clojure są kompilowane do bajtkodu
Java. Clojure "pod maską" używa standardowych typów i pasujących klas
języka Java (np. funkcje reprezentowane są przez obiekty implementujące
interfejsy Runnable i Callable).
Aby załagodzić ból korzystania z niekiedy barokowych konstrukcji i
bibliotek języka Java, Clojure daje tzw. lukier składniowy. Ułatwia on
typowe zadania, takie jak importowanie typów, czy ustawianie właściwości
obiektu.
Clojure - wielowątkowość
funkcyjny charakter to pewniejsza
podstawa do programowania
współbieżnego
Czas teraz na główne danie w menu - język funkcyjny Clojure jako lepsza
baza dla tworzenia programów działających wielowątkowo.
Jest kilka dużych zalet podejścia zorientowanego na funkcje w kontekście
programowania współbieżnego.
Clojure - wielowątkowość
funkcja matematyczna zawsze zwraca
ten sam wynik dla tych samych
danych
można je wykonywać równolegle
łatwiej ją testować
naturalna modularność programu
Pierwsza zaleta języków funkcyjnych to cecha funkcji w sensie
matematycznym. Zawsze zwracają ten sam wynik dla tych samych danych
wejściowych i z zasady ich wykonanie nie wpływa na wykonanie innych
funkcji w tym samym czasie. To znaczy że nie ma przeszkód, żeby
wykonywać wiele funkcji równolegle.
Dodatkowo, funkcje łatwiej testować, bo nie potrzeba martwić się o
kontekst wywołania (stan obiektu itp.).
Wreszcie podejście funkcyjne w naturalny sposób powoduje, że program
powstaje modularnie. Drobne funkcje wykonujące jedną rzecz, łączy się w
bardziej złożone, te w jeszcze bardziej złożone, aż w końcu gotowa
aplikacja pojawia się niemal sama.
Clojure - wielowątkowość
wywołanie funkcji z różnych wątków
jest bezpieczne
w językach czysto funkcyjnych - zawsze
Clojure nie jest czysto funkcyjny
...ale wiele mu nie brakuje
Funkcję można wykonywać bezpiecznie w różnych wątków, ponieważ z
zasady nie modyfikuje globalnego stanu. To druga duża zaleta języków
funkcyjnych. Jednocześnie jest to jednak ograniczenie. Bo modyfikowanie
globalnego stanu, czyli np. wypisanie czegoś do pliku, czasami się
przydaje.
W językach czysto funkcyjnych, takich jak Haskell, reguła ta jest
przestrzegana. Clojure nie jest aż tak dogmatyczny, jednak ilość miejsc
potencjalnie niebezpiecznych jest ograniczona do minimum.
Clojure - wielowątkowość
pamięć transakcyjna (ang. Software
Transactional Memory )
zasada dostępu z baz danych
synchroniczny dostęp do danej współdzielonej w
ramach transakcji
dostęp jest atomowy, spójny oraz izolowany (ACI)
Poza podejściem funkcyjnym, Clojure jest przyjazny dla programów
wielowątkowych na kilka innych sposobów.
Software Transactional Memory (STM) to pomysł na wykorzystanie zasady
dostępu do współdzielonego zasobu, znanego z systemów baz danych.
Tam nikt nie widzi nic dziwnego w tym, że tysiące równoczesnych żądań
dostępu do np. pojedynczej krotki tabeli nie powoduje zawieszenia
aplikacji, a każde z tych żądań zostaje obsłużone. Używa się tam skrótu
ACID, który znaczy że każdy dostęp jest atomowy, spójny, izolowany i
trwały.
STM w Clojure gwarantuje trzy pierwsze cechy.
Clojure - wielowątkowość
agenci
przekazywanie danych przez komunikaty (ang.
message passing concurrency )
zasada dostępu stosowana w językach Erlang,
Scala
asynchroniczny i niezależny dostęp danej
współdzielonej
Autor Clojure, Rich Hickey, stwierdził że obecnie STM oraz inne metodyki
mające rozwiązać problem pewnego programowania współbieżnego nie są
dostatecznie zweryfikowane w praktyce. Dlatego zaproponował wiele
takich metodyk na raz w Clojure.
STM dotyczy sytuacji dostępu synchronicznego. Dla sytuacji, kiedy
potrzebny jest dostęp asynchroniczny, dostępny jest mechanizm tzw.
agentów.
W największym skrócie to mechanizm przekazywania asynchronicznych
komunikatów między niezależnymi wątkami.
Clojure - wielowątkowość
atomy
synchroniczny i niezależny dostęp danej
współdzielonej
dana to jeden z niemodyfikowalnych typów
Clojure
modyfikacji ulega referencja na "atom", a nie
"atom" (dane) sam w sobie
Agenci sprawdzają się, kiedy dostęp do współdzielonych danych powinien
być asynchroniczny. Czasami jednak potrzebna jest synchronizacja
dostępu i wtedy do dyspozycji Clojure daje nam mechanizm tzw. atomów.
Clojure - wielowątkowość
dostęp do
współdzielonych synchroniczny
danych
skoordynowany
niezależny
asynchroniczny
-
STM
atomy
agenci
Te trzy różne mechanizmy składają się na kompletny zestaw narzędzi
programowania współbieżnego.
Clojure - co zapamiętać?
Clojure to język funkcyjny z
rodziny Lisp działający na
platformie JavaVM
programowalny język
programowania
z bardzo rozbudowanym
wsparciem dla
współbieżności
I wreszcie dochodzimy do podsumowania. Wymieniłem tutaj rzeczy, które
trzeba zapamiętać z tej prezentacji.
Dołożyłbym do tego jeszcze krótką informację - Clojure już jest używany w
praktyce, chociaż naturalnie nie sposób nazwać języka Clojure
popularnym. Portal http://flightcaster.com/ jest napisany w Clojure.
“Lisp is worth learning for the profound
enlightenment experience you will have
when you finally get it; that experience
will make you a better programmer for
the rest of your days, even if you never
actually use Lisp itself a lot.”
- Eric Steven Raymond
Powstaje jeszcze na koniec pytanie czy warto poznać Clojure? Moja
odpowiedź jest oczywista - tak. A na pytanie "dlaczego?", mogę zacytować
Erica Raymonda, znanego chociażby z artykułu "the Cathedral and the
Bazaar".
W wolnym tłumaczeniu: "Lisp warto poznać dla tego przenikliwego uczucia
olśnienia, jakiego doznasz kiedy naprawdę go poznasz; to doświadczenie
sprawi, że będziesz lepszym programistą, nawet jeśli nie będziesz już
więcej Lisp używał".
Chcesz więcej?
strona główna projektu Clojure
Clojure - functional programming for
the JVM
Clojure on blip.tv
Dla wszystkich zaintrygowanych tą prezentacją podaję zestaw
obowiązkowych do odwiedzenia adresów.
Dziękuję za uwagę
W prezentacji użyłem "Lisp - Alien technology" logo z http://www.lisperati.
com/logo.html oraz "Duke Open Source" logo z http://duke.kenai.com/ .
Obrazek serca pochodzi z http://www.pixabella.com/archives/340-clip-artfree.html .
Znak "Exclamation" pochodzi z http://www.flickr.
com/photos/itsaboyd/2961805797/ .
Zdjęcie IBM 704 pochodzi z Wikipedii dzięki Lawrence Livermore National
Laboratory.

Podobne dokumenty