01 listopada 2009

Programowanie w Clojure - część 1 - tytułem wstępu

Jedną książkę o Clojure mam już za sobą (patrz recenzja Book review: Programming Clojure), a kolejna jest w trakcie pisania - Practical Clojure (The Definitive Guide), więc jeśli miałbym poczuć klimaty programowania w języku funkcyjnym z użyciem Clojure, nie pozostaje mi nic innego jak po prostu zacząć w nim programować. Nie planuję wielkich programów, ale przynajmniej przećwiczę te elementy języka, które oznaczyłem sobie jako ciekawe podczas lektury Programming Clojure Stuarta Halloway'a.

Jest wiele cech różniących programowanie funkcyjne od programowania obiektowego, ale podstawowym mogłyby być "czyste" funkcje (ang. pure functions), których wynik działania zależy wyłącznie od parametrów wejściowych, a nie od aktualnego stanu aplikacji czy innych ukrytych źródeł danych możliwych do wykorzystania w implementacji funkcji. Wiemy, że te same dane dają ten sam wynik bez względu na moment, w jakim funkcja została wykonana. Programowanie z czystymi funkcjami nie jest obce programistom javowym. W Javie to my jednak decydujemy, czy tworzona funkcja jest czysta (bez efektów ubocznych), czy nie. Wszystko zależy od implementacji. W programowaniu funkcyjnym może to być w ogóle niemożliwe do realizacji, albo wymaga specjalnych słów kluczowych w języku, które explicite oznaczą funkcję, jako posiadającą efekty uboczne. Możnaby powiedzieć, że w Javie też tak jest, bo pewne konstrukcje mówią nam, czy skutkują efektem ubocznym, czy nie, ale to wymaga analizy implementacji, podczas gdy w Clojure jedynie wyszukania odpowiednich słów kluczowych obejmujących sekcje modyfikujące.

Kolejną składową programowania funkcyjnego są niezmienne struktury danych (ang. immutable data structure). Każda operacja na strukturze danych powoduje jej skopiowanie i wykonanie operacji. Każdorazowo dostajemy kopię struktury wejściowej.

Łącząc obie cechy programowania funkcyjnego, efekty uboczne (ang. side effects) nie wystąpują w ogóle, albo są wyjątkiem a nie regułą. Tym samym testowanie aplikacji jest prostsze. Sprawdzamy, czy dla tego samego wejścia mamy to samo wyjście i tyle. Podobnie z programowaniem współbieżnym (równoległym). Skoro wszystkie struktury są niezmienne, a działanie funkcji zwraca ich kopię, to nie ma czego synchronizować (!) Idąc dalej, programowanie funkcyjne daje możliwość zrównoleglania obliczeń, skoro wynik działania funkcji zależy wyłącznie od danych wejściowych, a kolejność obliczeń składowych funkcji złożonej nie ma znaczenia. I dalej, jeśli wykonamy funkcję czystą z parametrami wejściowymi, które będą wykorzystane w kolejnym wywołaniu, możemy zoptymalizować takie wywołanie podstawiając wynik poprzedniego wywołania funkcji - umieścić wynik w pamięci podręcznej i kosztem funkcji będzie jedynie odczyt z pamięci.

Nie ma nic za darmo. W końcu, gdyby taka słodycz płynęła z programowania funkcyjnego, zamiast programowania imperatywno-obiektowego w Javie pisalibyśmy aplikacje w Haskellu, OCamlu, MLu, czy microsoftowym F#. Coś jest na rzeczy, że bliżej nam do imperatywnego myślenia niż funkcyjnego, podobnie jak z bazami danych, które przyzwyczailiśmy się postrzegać relacyjnie, a oprogramowywać dostęp do ich danych obiektowo (stąd powód dla rozwiązań ORM). Dobrze jednak mieć wybór. Clojure jest językiem uruchamianym na JVM z całym dobrodziejstwem inwentarza, a wprowadza nas w świat programowania funkcyjnego, w którym niektóre problemy rozwiązuje się prościej. Miejmy po prostu świadomość istnienia Clojure, podobnie jak Groovy, Scala, JRuby, Jython, Erlang, itp. i dobierajmy narzędzie do problemu, a nie komplikujmy problem, aby dopasować i rozwiązać go znanymi technikami.

Nie mam jeszcze wyrobionego zdania nt. Clojure i jego użycia w projektach. Próbuję wierząc, że komentarze i dyskusje dadzą mi odpowiedź, czy jest warto. Jedna książka to zdecydowanie za mało, aby wyrobić sobie pogląd na temat zastosowania Clojure w projektach. Skoro znalazła się osoba Rich Hickey, która stworzyła kolejny język funkcyjny Clojure, tym razem działający na JVM, to musi w tym być jakaś ukryta wiedza, której zrozumienie nie jest mi jeszcze dane. Kładę to raczej na barki mojego intelektualnego niedorozwoju niż braku sensu w powstaniu Clojure. Kiedy będę mógł porównać Javę do Clojure i wskazać zalety jednego względem drugiego, nawet jeśli skończy się na wyrzuceniu Clojure jako niepotrzebnego, mam nadzieję, że mimo wszystko nie będzie to czas stracony. Podkreślam słowo nadzieja. Aktywność na grupie dyskusyjnej użytkowników Clojure wskazuje, że język ma swoje poletko, w którym sprawdza się. Chcę wiedzieć, czy dla mnie również.

Ciekawym podsumowaniem cech programowania funkcyjnego może być prezentacja Programowanie funkcyjne - wprowdzenie p. dr inż. Marcina Szlenka z PW. W katalogu spop, gdzie znajduje się prezentacja, znajdziemy 2 zadania, które możemy spróbować zrealizować w Clojure (autor zażyczył sobie implementacji w Haskellu). Zawsze to łatwiej uczyć się nowego mając określone zadanie, poza takim ogólnym jak po prostu nauczyć się nowego :)

Clojure to Lisp na JVM. Składniowo oznacza to mnóstwo nawiasów, aczkolwiek twórca Clojure zrozumiał, że była to jedna z bolączek Lispa i w wielu miejscach, gdzie Lisp wymagałby nawiasów, w Clojure ich nie ma. I dobrze.

W książce "Programming Clojure" autor napisał:

"My personal quest for a better JVM language included significant time spent with Ruby, Python, and JavaScript, plus less intensive exploration of Scala, Groovy, and Fan. These are all good languages, and they all simplify writing code on the Java platform. But for me, Clojure stands out."

Interesujące, co? Nie potrafiłbym porównać wszystkich wymienionych języków, więc nie pozostaje mi nic innego, jak uwierzyć autorowi na słowo i samemu spróbować swoich sił z Clojure. Ciekawe jest, że obok Clojure pojawiły się również Scala, Groovy i nieznany mi w ogóle Fan. Wystarczy chwila na stronie Fan i trudno nie oprzeć się wrażeniu, że wszystko w nim jest, a jednak brakuje mu zainteresowania społeczności. Dlaczego? Działa na JVM, .Net CLR i w przeglądarce. Ma jakby wszystko, co daje Java, a środowisko szersze, a jednak czegoś brakuje. Ktoś parał się nim?

Wracając do Clojure, zacznijmy od początku. Rozpoczynamy programowanie w Clojure pobierając kompilator i środowisko uruchomieniowe ze strony domowej Clojure. Pobieramy Clojure 1.0.0 i rozpakowujemy w wybranym katalogu. Ja wybrałem trochę bardziej pokrętną drogę i zbudowałem Clojure lokalnie, więc numery wersji będą różne. Dla zainteresowanych, wystarczy
 svn co http://clojure.googlecode.com/svn/trunk/ clojure
cd clojure
ant
Uruchamiamy interpreter Clojure - REPL - poleceniem
 jlaskowski@work /cygdrive/c/oss/clojure
$ java -jar clojure-1.1.0-alpha-SNAPSHOT.jar
Clojure 1.1.0-alpha-SNAPSHOT
user=>
Jest to podobne do groovysh czy groovyConsole w Groovy i pewnie podobne rozwiązania znajdziemy również w Scali czy Erlangu. Składnia Clojure wymaga, aby każde polecenie było podawane w nawiasach, w których najpierw podajemy funkcję, a później jej parametry, np.
 user=> (+ 1 3)
4
user=> (- 3 1)
2
user=> (* 2 4)
8
Jeśli wymagane jest dalsze zagnieżdzenie struktur, mamy kolejny poziom nawiasów, itp.
 user=> (+ 2 (- 5 2))
5
Z pewnymi wyjątkami nawiasy wyznaczają zakres działania funkcji. Skoro wszystko jest funkcją, będzie tyle par nawiasów, co wykonywanych funkcji.

Koniec pracy w REPL to znane i lubiane Ctrl-C. To byłoby tyle na dobry początek. Zainteresowany? A czym?! :) W kolejnych wpisach dalsze podboje programowania funkcyjnego z Clojure przy tworzeniu czegoś bardziej użytecznego niż przysłowiowe "Witaj Świecie".