Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Projekt Clojure z Apache Maven

Do budowania projektów Clojure mamy do wyboru Maven'a i Leiningen'a. W tym poście opiszę jak można stworzyć prosty projekt przy użyciu tego pierwszego. Wykorzystałem:

  • Eclipse Juno z wtyczką Counterclockwise
  • Maven 3

Stworzenie projektu

Wtyczka Counterclockwise umożliwia utworzenie projektu Clojure w Eclipse. Jednak jeśli chcemy projekt oparty o Mavena, to myślę, że najlepiej zacząć tworząc zwykły projekt Javovy. Naciśnij Alt+Shift+N, wybierz Java Project. Podczas tworzenia projektu dodaj dwa katalogi na źródła: src/main/clojure, src/test/clojure.

Dodaj pom.xml

Dodaj plik pom.xml o treści:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>pl.mpr</groupId>
 <artifactId>clojure-example</artifactId>

 <packaging>clojure</packaging>

 <version>1.0-SNAPSHOT</version>
 <name>clojure-example</name>
 <url>http://maven.apache.org</url>
 <build>
  <plugins>
   <plugin>
    <groupId>com.theoryinpractise</groupId>
    <artifactId>clojure-maven-plugin</artifactId>
    <version>1.3.12</version>
    <extensions>true</extensions>
   </plugin>
  </plugins>
  <testResources>
   <testResource>
    <directory>${basedir}/src/test/clojure</directory>
   </testResource>
  </testResources>
 </build>
 <dependencies>
  <dependency>
   <groupId>org.clojure</groupId>
   <artifactId>clojure</artifactId>
   <version>1.4.0</version>
  </dependency>
  <dependency>
   <groupId>org.clojure</groupId>
   <artifactId>clojure-contrib</artifactId>
   <version>1.2.0</version>
  </dependency>
 </dependencies>
</project>
Wykonaj:
mvn eclipse:eclipse
i odśwież projekt w Eclipse. W tym momencie powinno być dodane wsparcie dla Clojure:

Dodanie testu i kodu w Clojure

Zgodnie z prawidłami TDD zaczniemy od napisania testu. Będzie to test dla funkcji zwracającej łańcuch tekstowy "Hello world!". Stwórz w katalogu na testy paczkę, a w nim plik HelloTest.clj:

(ns pl.mpro.clojureexample.HelloTest
  (:use pl.mpro.clojureexample.Hello)
  (:use clojure.test))

(deftest get-hello-test
  (let [hello "Hello world!"]
    (is (= (get-hello) hello))))

Wybieramy z menu: Clojure->Run file in Repl. Oczekiwany rezultat to:

Oczywiście testujemy coś, czego jeszcze nie jest napisane, stąd powyższy błąd. Teraz czas na implementację. Stwórz w katalogu src/main/clojure plik Hello.clj:

(ns pl.mpro.clojureexample.Hello)

(defn get-hello []
  "Hello world!")

Ponownie załaduj HelloTest.clj. Tym razem się to powinno udać. W repl'u wpisz

(run-tests)
Spodziewany rezultat to:
Testing pl.mpro.clojureexample.HelloTest

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
{:type :summary, :pass 1, :test 1, :error 0, :fail 0}
W oknie REPL możesz pisać kod clojure i zostanie on od razu wykonany. Jest jeszcze jedna wygodna funkcjonalność. W pliku Hello.clj dopisz:
(println "Test")
zaznacz i naciśnij Ctrl+Enter. To powinno wykonać w REPL zaznaczony kod.

Wykonanie testów przy użyciu Mavena

Przy obecnej konfiguracji, do wykonania testów clojure powinno wystarczyć:

mvn test
A

Testing pl.mpro.clojureexample.HelloTest

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 13.812s
[INFO] Finished at: Thu Nov 15 00:20:20 CET 2012
[INFO] Final Memory: 6M/16M
[INFO] ------------------------------------------------------------------------
Jeśli się to nie powiodło, sprawdź, czy w pliku pom.xml jest:
<packaging>clojure</packaging>
Skompilować projekt można używając standardowo:
mvn package

http://evo-java.googlecode.com/svn/clojure-example


Co lubię w Clojure cz.2

Kolejną fajną rzeczą w Clojure są funkcje wyższego rzędu map, filter, reduce. (funkcje wyższego rzędu to takie które przyjmują inne funkcje jako argument, lub ich wartością zwracaną jest funkcja).

Zanim podam przykłady użycia tych funkcji, wspomnę, że w Clojure wygodniej niż w Javie tworzy się listy, mapy, zbiory i wektory:

(def some-list1 '(1 2 3 4 5))
(def some-list2 (list 1 2 3 4 5))
(def some-vector1 [1 2 3 4 5])
(def some-vector2 (vec 1 2 3 4 5))
(def some-set1 #{1 2 3 4 5})
(def some-set2 (hash-set 1 2 3 4 5))
(def some-map1 {:a 1 :b 2 :c 3 :d 4})
(def some-map2 (hash-map :a 1 :b 2 :c 3 :d 4))

Funkcja filter

Zwraca sekwencję elementów, pochodzącej z danej kolekcji, które spełniają określony warunek. Przykładowo, chcemy liczby pierwsze z wektora some-vector1. Zdefiniujmy funkcję zwracającą true dla liczb pierwszych, false dla złożonych:

(defn prime? [x]
     (if (> x 1)
       (let [divisors
             (->> x (Math/sqrt) (int) (inc) (range 2))]
         (not (some #(zero? (mod x %)) divisors))
         )
       false))
Mając taką funkcję, przefiltrowanie wektora można zapisać w jednej linijce:
(filter prime? some-vector1) ; rezultat: (2 3 5)

Funkcja map

Ta funkcja przyjmuje dwa argumenty: inną funkcję i kolekcję. Rezultatem jest sekwencja elementów, powstałych na skutek zastosowania przekazanej w argumencie funkcji na elementach kolekcji. Zatem, mając wektor liczb, możemy w jednej linijce podnieść wszystkie do kwadratu:

(map #(* % %) some-vector1) ; rezultat: (1 4 9 16 25)
Programując w Javie często spotykam się z przypadkiem, że mając listę obiektów, potrzebuję utworzyć listę, w której każdy element to określone pole. Na przykład z listy osób stworzyć listę ich imion. W clojure coś takiego robi się bardzo elegancko:
(def peoples [{:name "Jan" :age 20} {:name "Tomasz" :age 15} {:name "Conan" :age 60}])
(map :name peoples) ; rezultat: ("Jan" "Tomasz" "Conan")

Funkcja reduce

Kolejną ciekawą funkcją wyższego rzędu jest reduce. Działa w ten sposób, że stosuje funkcję podaną w argumencie na pierwszych dwóch elementach kolekcji, potem na wyniku i trzecim elemencie itd. Przykład

(reduce + some-vector1) ; rezultat 15

Leniwe sekwencje

Rezultatem funkcji map jest leniwa sekwencja. Oznacza to, że żaden element tej sekwencji nie jest obliczany, dopóki nie zostanie użyty.

Wcześniej napisana funkcja prime? dla większych liczb wykonuje się dosyć długo:

(time (prime? 200560490131)) ; "Elapsed time: 51.440427 msecs"
(time (prime? 2147483647)) ; "Elapsed time: 4.971057 msecs"
(time (prime? 87178291199)) ; "Elapsed time: 84.389674 msecs"

dlatego użyję jej w przykładzie.

(def prime-vec [87178291199 200560490131 2147483647])
(time (def prime-bools (map prime? prime-vec))) ; "Elapsed time: 0.057807 msecs"
(time (nth prime-bools 1)) ; "Elapsed time: 31.690193 msecs"
(time (nth prime-bools 1)) ; "Elapsed time: 0.029071 msecs"

Funkcja memoize

Za pomocą memoize można stworzyć wersję funkcji, która "zapamiętuje" parametry z jakim została wywołana i zwracane wartości, dzięki czemu, gdy zostanie wywołana potem z takimi samymi argumentami, wynik nie musi być liczony od nowa. Przykład:

(def cached-prime? (memoize prime?))
(time (cached-prime? 200560490131)) ;"Elapsed time: 275.652701 msecs"
(time (cached-prime? 200560490131));"Elapsed time: 0.105393 msecs"

Co lubię w Clojure, cz. 1

Zachęcony zwiększającą się popularnością języka Clojure, postanowiłem zapoznać się z jego możliwościami. Przy okazji, opublikuję serię postów, w których pokażę co ciekawego oferuje ten język.

Clojure działa na JVM, jest dialektem Lisp'a i językiem funkcyjnym. Różni się od Javy o wiele bardziej niż Java od C++. O technicznych cechach języka można poczytać na [clojure.pl], polecam również zajrzeć na [hammerprinciple.com], gdzie zobrazowane są opinie internautów na temat Clojure w porównaniu z Javą. Rzuca się w oczy to, że kod w Clojure uważany jest za bardziej zwięzły - ja mam podobne odczucia. Przejdźmy teraz do praktyki.

Szybki start

By zacząć pisać w Clojure, wystarczy ściągnąć blibliotekę z [clojure.org] i wykonać:

java -jar clojure-1.4.0.jar

To uruchomi REPL, w którym można pisać kod i zostanie on od razu wykonany. Inna możliwość to wypróbować REPL online na stronie [tryclj.com].

Funkcje jako argumenty innych funkcji

Zatem, co jest ciekawego w Clojure, czego nie ma w Javie? Np to, że funkcje mogą być przekazywane jako argumenty do innej funkcji. W rezultacie, coś co w Javie uzyskujemy poprzez wzorzec strategii, w Clojure jest czymś zupełnie naturalnym i prostym. Prosty przykład:

public class StrategySnippet {
 public static void main(String[] args) {
  new StrategySnippet().go();
 }

 public void go() {
  useStrategy(new AddStrategy(), 2, 3);
  useStrategy(new MultiplyStrategy(), 2, 3);
 }

 public void useStrategy(Strategy s, int a, int b) {
  System.out.println(s.execute(a, b));
 }

 interface Strategy {
  int execute(int a, int b);
 }

 class AddStrategy implements Strategy {

  @Override
  public int execute(int a, int b) {
   return a + b;
  }

 }

 class MultiplyStrategy implements Strategy {

  @Override
  public int execute(int a, int b) {
   return a * b;
  }

 }
}

W Clojure analogiczny kod to:

(defn use-strategy[strategy a b]
  (println (strategy a b)))

(defn add-strategy [a b]
  (+ a b))

(defn multiply-strategy [a b]
  (* a b))

(defn go []
  (use-strategy add-strategy 2 3)
  (use-strategy multiply-strategy 2 3)
 )
(go)

W dodatku + i * same w sobie są funkcjami, zatem taki sam efekt uzyskamy poprzez:

(defn go []
  (use-strategy + 2 3)
  (use-strategy * 2 3)
 )
(go)
W kolejnym przykładzie kod sortujący listę String'ów po ich długości. W Javie użyjemy metodę Collection.sort i anonimową klasę implementującą Comparator:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class AnonymousClassSnippet {

 public final static List<String> SOME_LIST = Arrays.asList(new String[] { 
"Jan", "Zbych", "Boromir", "Urlich","Rupert","Jaro" });

 public void sortAndPrint(List<String> list) {
  ArrayList<String> l = new ArrayList<String>(list);
  Collections.sort(l, new Comparator<String>() {

   @Override
   public int compare(String arg0, String arg1) {
    if (arg0.length() > arg1.length())
     return 1;
    else if (arg0.length() < arg1.length())
     return -1;
    else
     return 0;
   }

  });
  System.out.println(l);
 }
 public static void main(String []args){
  new AnonymousClassSnippet().sortAndPrint(SOME_LIST);
 }
}
W Clojure można zapisać to tak:
(def some-vector ["Jan" "Zbych" "Boromir" "Urlich" "Rupert" "Jaro"])
(defn sort-and-print [vec]
  (println 
    (sort #(compare (.length %1) (.length %2)) vec)
  ))
(sort-and-print some-vector)
Użyta tu została funkcja anonimowa - zapisujemy ją:
#( ciało_funkcji)

przy czym %1 i %2 to parametry. Prawda, że krócej?