"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 Easy 1

"SQL, Lisp, and Haskell are the only programming languages that I've seen where one spends more time thinking than typing."
- Philip Greenspun


Na początek zagadka: Ilu inżynierów od sprzętu komputerowego potrzeba by wymienić żarówkę?
Odpowiedź na końcu wpisu.

W tym cyklu nie będzie poznawania podstawowych funkcji, a raczej ich zastosowanie w akcji. Przewiną się wszystkie poznane już w poprzedniej części konstrukcje oraz pojawią się bardziej zaawansowane funkcje. Będzie więcej kombinowania. Dodatkowo pojawią się przeszkody w postaci zakazów użycia niektórych funkcji.

Zalecam użycie Eclipse (dowolna edycja, może być C++, bo mało zajmuje) oraz wtyczkę Counterclockwise (do Clojure), którą można zainstalować z poziomu Eclipse poprzez Help->Eclipse Marketplace... Po utworzeniu projektu Clojure należy utworzyć namespace np. core. Potem uruchomić przyciskiem Run. Uruchomi się w ten sposób linia poleceń Clojure, gdzie można testować kod. Jeżeli program wpadnie w nieskończoną pętlę, bądź się przywiesi, to można go zatrzymać przy pomocy ikonki z trybikiem lub Ctrl+I.

Problem 1: Last Element


Zadanie polega na wypisaniu ostatniego elementu kolekcji. Aby zadanie nie było zbyt łatwe nie można użyć funkcji last. Oczywiście nie utrudnia to nam zadania.
T1: (= (__ [1 2 3 4 5]) 5)
T2: (= (__ '(5 4 3)) 3)
T3: (= (__ ["b" "c" "d"]) "d")

E1: #(first (reverse %))

Problem 2: Penultimate Element


Jeszcze trudniejsze zadanie. Tym razem trzeba podać wartość drugiego elementu od końca.

T1: (= (__ (list 1 2 3 4 5)) 4)
T2: (= (__ ["a" "b" "c"]) "b")
T3: (= (__ [[1 2] [3 4]]) [1 2])

E1: #(second (reverse %))
E2: #(last (butlast %))

Problem 3: Nth element


Zadanie polega na podaniu n-tego elementu kolekcji przekazanej do funkcji. Jest jednak haczyk. Nie można użyć funkcji nth.

Funkcja take bierze n elementów z listy. Jednak autor testu przyjął podstawę 0. Stąd funkcja musi wziąć o jeden element więcej. Szukany element będzie na ostatniej pozycji z obciętej listy.

T1: (= (__ '(4 5 6 7) 2) 6)
T2: (= (__ [:a :b :c] 0) :a)
T3: (= (__ [1 2 3 4] 1) 2)
T4: (= (__ '([1 2] [3 4] [5 6]) 2) [5 6])

E1: #(last (take (inc %2) %1))

Problem 4: Count a Sequence


Zadanie polega na wypisaniu ilości elementów listy bez użycia funkcji count
Pierwszy sprytny sposób to nudne zliczanie każdego elementu w pętli : E1.
Drugi sprytny sposób: E2, to utworzenie listy jedynek za pomocą funkcji map, które potem będą zsumowane.

T1: (= (__ '(1 2 3 3 1)) 5)
T2: (= (__ "Hello World") 11)
T3: (= (__ [[1 2] [3 4] [5 6]]) 3)
T4: (= (__ '(13)) 1)
T5: (= (__ '(:a :b :c)) 3)

E1: #(loop [c 0 l %]
        (if (empty? l)
            c
            (recur (inc c) (rest l))))
E2: #(reduce + (map (fn [e] 1) %))

Problem 5: Sum it all up


Zadanie polega na napisaniu funkcji, która zwróci sumę wszystkich elementów.
Dużo pracy tu nie ma, bo wykorzystam poprzednią funkcję, która zamiast zliczać jedynki będzie sumować poszczególne elementy.

T1: (= (__ [1 2 3]) 6)
T2: (= (__ (list 0 -2 5 5)) 8)
T3: (= (__ #{4 2 1}) 7)
T4: (= (__ '(0 0 -1)) -1)
T5: (= (__ '(1 10 3)) 14)

E1: #(loop [sum 0 l %]
         (if (empty? l)
             sum
             (recur (+ sum (first l)) (rest l))))
E2: #(apply + %)
E3: #(reduce + %)

Problem 6: Find the odd numbers



Zadanie polega na znalezieniu wszystkich elementów nieparzystych z podanej listy i zwrócenie ich jako listy. Znając już makro for zadanie będzie dość proste (E1). Zmodyfikuję też poprzednią pętlę i podam jako E2.

T1: (= (__ #{1 2 3 4 5}) '(1 3 5))
T2: (= (__ [4 2 1 6]) '(1))
T3: (= (__ [2 2 4 6]) '())
T4: (= (__ [1 1 1 3]) '(1 1 1 3))

E1: #(for [x % :when (odd? x)] x)
E2: #(loop [wyn [] lst %]
            (if (empty? lst)
                wyn
                (recur (if (odd? (first lst))
                           (conj wyn (first lst))
                           wyn)
                        (rest lst))))

Problem 7: Reverse a Sequence


Zadanie polega na zmianie kolejności elementów kolekcji bez używania funkcji reverse i rseq.

T1: (= (__ [1 2 3 4 5]) [5 4 3 2 1])
T2: (= (__ (sorted-set 5 7 2 7)) '(7 5 2))
T3: (= (__ [[1 2][3 4][5 6]]) [[5 6][3 4][1 2]])

E1: #(loop [wyn [] lst %]
               (if (empty? lst)
                   wyn
                   (recur (conj wyn (last lst))
                          (butlast lst))))


Problem 8: Palindrome detector.


Zadanie polega na napisaniu funkcji, która testuje czy podana sekwencja jest palindromem.
Ciąg jest palidromem, jeżeli pierwsza połowa sekwencji jego elementów jest równa odwróconej drugiej połowie. Jeżeli liczba elementów ciągu jest nieparzysta środkowy znak się ignoruje. Należy więc wziąć liczbę n elementów ciągu , podzielić całkowicie przez dwa (funkcja quot), wziąć n/2 pierwszych elementów , odwrócić listę, wziąć n/2 pierwszych elementów i porównać listy (E1). Drugi sposób to porównywać pierwszy element z ostatnim (przy okazji usuwać te elementy), aż lista nie będzie pusta (E2). Trzeci sposób to wziąć sobie jakiś licznik i porównywać n-ty z k-n element listy, gdzie k to długość listy, a n: <0, k-1>.

T1: (false? (__ '(1 2 3 4 5)))
T2: (true? (__ "racecar"))
T3: (true? (__ [:foo :bar :foo]))
T4: (true? (__ '(1 1 3 3 1 1)))
T5: (false? (__ '(:a :b :c)))

E1: #( let [lst %
        n (quot (count lst) 2)
        pp (take n lst)
        dp (take n (reverse lst))]
       (if (< n 1)
           true
          (reduce (fn [x y] (and x y)) (map (fn [a b] (= a b)) pp dp))))

E2: #( let [lst %
        n (quot (count lst) 2)]
        (loop [f (first lst) l (last lst) tmp lst]
          (if (empty? tmp) true
          (if (not= f l)
               false
              (recur (second tmp) (last (butlast tmp)) (rest (butlast tmp)))))))

E3: #( let [v (vec %)
           k (count v)
           n (quot k 2)]
           (loop [f 0 l (dec k)]
             (if (>= f n)
                 true
                 (if (not= (v f) (v l))
                     false
                     (recur (inc f) (dec l))))))

------------------------------------------------------------------------------------------------------------
Odpowiedź na zagadkę: Żadnego. Problem obejdzie się w sofcie. 

Clojure Elementary 4


"If I had a nickel for every time I've written "for (i = 0; i < N; i++)" in C I'd be a millionaire."

- Mike Vanier



Po części trzeciej, czas na ostatnią, czwartą część łamigłówek o podstawowym stopniu trudności. W kolejnej serii przejdę do stopnia trudności określanego jako Easy.


Taka mała zagadka: Ilu programistów .NET potrzeba, by zabić karalucha?
(odpowiedź na końcu wpisu.).

Problem 27: for the win


Zadanie polega na odgadnięciu wyniku działania makra for.
Makro to bierze za pierwszy argument wektor powiązań (podobnie jak let i opcjonalnie trzy modyfikatory:
- :let [ powiązania dla dodatkowych zmiennych pomocniczych ]
- :when ( test ) - zmienne w iteracji zostaną przekazane po spełnieniu testu
- :while ( test ) - for zakończy działanie po negatywnym wyniku testu

Powiązania (nie dotyczy :let) muszą być sekwencjami np.: (for [x '(3 4) z [1 2]] (...) ). Elementy sekwencji będą łączone na zasadzie każdy z każdym. To jest dla każdego elementu x zostanie przypisany każdy element z i taka postać przekazana jako parametry do bloku wykonania. Blok wykonania zwraca wartość, z której po każdej iteracji będzie budowana sekwencja. Tak, że for służy do budowania sekwencji na podstawie innych sekwencji.

W teście T1 mam for, które będzie się wykonywać dla każdego x z sekwencji zbudowanej przy pomocy funkcji range). W tym przypadku range zwróci kolekcję liczb od 0 do 39 włącznie z krokiem 1. Iterując po każdym elemencie zostaje sprawdzony warunek :when, który zwraca prawdę jeżeli reszta z dzielenia elementu x kolekcji przez 4 (funkcja rem będzie równa 1. Dzięki temu zostanie zbudowana kolekcja elementów, których reszta z dzielenia przez 4 jest 1. Wynik w ostateczności powinien wyglądać tak: (1 5 9 13 17 21 25 29 33 37).

W teście T2 x jest nieskończoną kolekcją zwróconą przez funkcję iterate. Funkcja ta bierze za pierwszy argument funkcję, a za drugi wartość początkową. Funkcja anonimowa zwraca wartość argumentu powiększonego o 4. Stąd x będzie mieć postać: (0 4 8 12 ... ). Modyfikator :let tworzy zmienną pomocniczą z, która będzie mieć wartość elementu x powiększonego o 1. Modyfikator :while sprawdza, czy z jest mniejsze od 40, jeżeli nie, to kończy działanie for

W teście T3 for iteruje po parach liczb utworzonych poprzez utworzenie kolekcji liczb
(0 1 2 3 ... 19) podzielonej na kolekcję dwuelementowych list : ( (0 1) (2 3) (4 5) .... (18 19) ).
Wynikiem jest lista, której elementy to sumy par liczb.
Pary i większe kolekcje liczb można przypisywać do zmiennych lokalnych w rózny sposób, np:

(let [ x 1 y 2 ] (+ x y))
i
(let [ [x y] '(1 2) ] (+ x y))
vs
(let [ x (first '(1 2)) y (second '(1 2)) ] (+ x y))
I zadanie:

T1: (= __ (for [x (range 40)
            :when (= 1 (rem x 4))]
        x))
T2: (= __ (for [x (iterate #(+ 4 %) 0)
            :let [z (inc x)]
            :while (< z 40)]
        z))
T3: (= __ (for [[x y] (partition 2 (range 20))]
        (+ x y)))

E1: '(1 5 9 13 17 21 25 29 33 37).
E2: [1 5 9 13 17 21 25 29 33 37].

Problem 28: Logical falsity and truth


W Clojure w testach logicznych tylko nil i false zwracają logiczny fałsz, wszystko inne ewaluuje się do logicznej prawdy. Funkcja if Służy do wykonywania dwóch rodzajów kodu w zależności od wyniku przekazanego do niej testu. Za pierwszy argument bierze test, który zwraca prawdę lub fałsz. Jeżeli wynikiem testu będzie prawda, zostanie wykonany kod podany w drugim argumencie w przeciwnym wypadku zostanie wykonany kod w trzecim. Jeżeli trzeciego argumentu nie będzie, a test sugeruje na jego wykonanie, to if zwróci nil.

T1: (= __ (if-not false 1 0))
T2: (= __ (if-not nil 1 0))
T3: (= __ (if true 1 0))
T4: (= __ (if [] 1 0))
T5: (= __ (if [0] 1 0))
T6: (= __ (if 0 1 0))
T7: (= __ (if 1 1 0))
  
E1: 1

Problem 29: Map defaults


W przypadku pozyskiwania wartości rezydującej pod kluczem mapy, oprócz sposobu jaki był przedstawiony w problemie nr 26 jest jeszcze jedna metoda na poradzenie sobie z sytuacją w której dany klucz nie istnieje. Można do wyrażenia pobierającego wartość spod klucza dodać wartość domyślną, która zostanie zwrócona, gdy klucz nie istnieje, np.:

(:k {:a 0, :b 1, :c nil} :not_exist)
Zwróci :not_exist.

A co jeżeli chcemy utworzyć mapę i nadać kluczom jakaś domyślną wartość?
Zadanie polega na utworzeniu funkcji, która pobierze sekwencję kluczy, wartość domyślną i utworzy z nich mapę. Mapę w najprostszy sposób tworzy się poprzez (hash-map klucz dane) lub {klucz dane}. Wiadomo, że przez elementy sekwencji można iterować przez użycie map lub for. Funkcja map bierze za parametr funkcję, którą stosuje dla każdego elementu sekwencji. Stąd mając domyślą wartość klucza i sekwencję kluczy można zwrócić sekwencję map. Złączyć to można poleceniem conj i funkcją apply lub reduce.

Rozpiszę główne etapy:
1. map ... : ({:a 0} {:b 0} {:c 0})
2. apply conj ... : {:a 0 :b 0 :c 0}
z czego apply tworzy listę w sposób: (conj {:a 0} {:b 0} {:c 0})
reduce: (conj {:a 0} {:b 0}) -> (conj {:a 0 :b 0} {:c 0}) ...

Zadanie:

(= (__ 0 [:a :b :c]) {:a 0 :b 0 :c 0})
(= (__ "x" [1 2 3]) {1 "x" 2 "x" 3 "x"})
(= (__ [:a :b] [:foo :bar]) {:foo [:a :b] :bar [:a :b]})

E1: #(reduce conj (map (fn [d] {d %1}) %2))
E2: #(apply conj (map (fn [d] {d %1}) %2))
------------------------------------------------------------------------------------------------------------
Odpowiedź na zagadkę: Dwóch. Jeden trzyma karalucha, by nie uciekł, a drugi instaluje na nim Windows.

Clojure Elementary 3

“Don't worry about what anybody else is going to do. The best way to predict the future is to invent it.”

— Alan Kay 
Kolejna część zmagań z zadaniami ze strony 4Clojure.
Do dzieła!: ;)



Problem 17: Sequences : filter.

Zadanie polega na zgadnięciu rezultatu filtrowania listy za pomocą funkcji testującej. Funkcja filter bierze za pierwszy argument funkcję testującą a drugi sekwencję. Zwraca elementy sekwencji, które przejdą test pozytywnie. Tutaj funkcja anonimowa zwraca logiczną prawdę jeżeli przekazany do niej parametr jest większy od 5, a więc filter powinien zwrócić wszystkie elementy sekwencji większe od 5.
T1: (= __ (filter #(> % 5) '(3 4 5 6 7)))
E1: '(6 7)
E2: [6 7]


Problem 18: Local bindings.


Funkcja let pozwala na lokalne przypisanie nazw funkcjom bądź danym. Bardzo ułatwia czytanie kodu. Budowa jest postaci:
(let [zmienna1 funkcja1 zmienna2 funkcja2 .... zmiennaN funkcjaN] (obliczena z wykorzystaniem zmienna1..N))
Zadanie polega na odgadnięciu rezultatu działania funkcji.
T1: (= __ (let [x 5] (+ 2 x)))
T2: (= __ (let [x 3, y 10] (- y x)))
T3: (= __ (let [x 21] (let [y 3] (/ x y))))
E1: 7


Problem 19: Let it Be


Zadanie odwrotne do zadania 18. Zamiast zgadywać wynik, trzeba zgadnąć postać przypisania. Zadanie jest trudne bo trzeba zgadnąć wartości przypisań. Najlepiej zacząć od dołu (T3).
T1: (= 10 (let __ (+ x y)))
T2: (= 4 (let __ (+ y z)))
T3: (= 1 (let __ z))
E1: [z 1 y 3 x 7]

Problem 20: Regular Expressions


Wyrażenia regularne. Tym razem trzeba zgadnąć rezultat działania funkcji re-seq, która za pierwszy parametr bierze postać wyrażenia regularnego, a jako drugi ciąg znaków do przeszukania. Wyrażenie regularne w Clojure ma postać: #" ". re-seq zwraca każde poprawne wystąpienie tekstu spełniającego wyrażenie w postaci sekwencji. Funkcję str już znamy. Funkcja apply natomiast powoduje użycie listy jako argumentów do funkcji str.
Zamiast:
(str "a" "b" "c")
jest:
(apply str '("a" "b" "c"))
I zadanie:
T1: (= __ (apply str (re-seq #"[A-Z]+" "bA1B3Ce ")))
E1: "ABC"

Problem 21: Intro to Reduce



Funkcja reduce bierze dwa lub trzy argumenty. Pierwszym jest funkcja, która może przyjąć dwa argumenty. Jeżeli drugim argumentem jest kolekcja, reduce bierze pierwsze dwa elementy z kolekcji i przekazuje je do funkcji. Jeżeli są jeszcze elementy w kolekcji, to brany jest wynik poprzedniej operacji na elementach, kolejny element z listy i przekazywane do podanej funkcji aż do wyczerpania elementów. Jeżeli zamiast kolekcji podamy jakąś wartość i kolekcję, to pierwszym elementem przekazanym do funkcji będzie ta wartość, a drugim pierwszy element z kolekcji.
Zadnie polega na zgadnięciu postaci funkcji jaka zastosowana według powyższych reguł da podany wynik.
T1: (= 15 (reduce __ [1 2 3 4 5]))
T2: (=  0 (reduce __ []))
T3: (=  6 (reduce __ 1 [2 3]))
E1: +
E2: (fn add
      ([] 0)
      ([a b] (+ a b)))
Uwaga. W odpowiedzi użyłem dziwnie wyglądającej funkcji anonimowej. Wcześniej napisałem, że funkcja reduce potrzebuje za pierwszy argument funkcji, która potrafi przyjąć dwa argumenty. Niestety, test T2 wymaga, by funkcja radziła sobie z brakiem argumentów. Clojure pozwala skonstruować funkcje tak, by w jednej definicji przewidywały odpowiedź na różną ilość argumentów. Te definicje oddziela się nawiasami okrągłymi i mogą być w dowolnej kolejności.

Problem 22: Simple Recursion


Rekurencja. Występuje wtedy, gdy funkcja wywołuje samą siebie. W tym zadaniu funkcja foo bierze liczbę za argument x. Jeżeli x jest większe od 0 (when) wykonuje funkcję conj, która tworzy sekwencję z funkcji foo pomniejszonej o 1 i aktualnego x. Funkcja conj łączy do początku listy, ale wykona się rekurencyjnie dopiero po napotkaniu warunku brzegowego (x > 0 ). Jeżeli x będzie 0 funkcja when zwróci nil. conj z nil i x daje (x). Ostatnim x przed warunkiem jest 1, pierwszym jest 5. Czyli łącząc do początku listy od 1 do 5 mamy listę w kolejności odwrotnej.

T1: (= __ (   (fn foo [x]
                      (when (> x 0)
                            (conj (foo (dec x))
                                  x)))
               5 ))

E1:  '(5 4 3 2 1)
E2: [5 4 3 2 1]


Problem 23: Rearranging Code: ->


Makro -> pozwala wykonywać na danych operacje sekwencyjnie zamiast zagnieżdżać w nawiasy. Co my tutaj mamy?

Funkcja reverse odwraca kolejność elementów w kolekcji, czyli będzie: [6 3 1 4 5 2]. rest zwraca wszystkie elementy oprócz pierwszego, więc: [3 1 4 5 2]. sort porządkuje elementy w kolejności rosnącej: [1 2 3 4 5]. Zadanie polega na zgadnięciu ostatniej funkcji, która po zastosowaniu na ostatnim wyniku zwróci 5.

T1: (= (__ (sort (rest (reverse [2 5 4 1 3 6]))))
       (-> [2 5 4 1 3 6] reverse rest sort __)
       5)

E1: last
E2: #(first (reverse %))

Uwaga, może nie zadziałać na 4Clojure mimo, że odpowiedzi są poprawne.

Problem 24: Recurring Theme



Clojure posiada konstrukcję, która pozwala na zapętlanie bez zapychania stosu - taki odpowiednik for. W językach takich jak C, Java kolejne wywołania funkcji powodują, że na stosie odkładane są kolejne ich parametry. Dodatkowo na stosie odkładane są zmienne lokalne funkcji. Jako, że stos nie jest nieskończony może się dość szybko zapchać, szczególnie, gdy stosujemy pętle rekurencyjne, które polegają na wywołaniach funkcji przez samą siebie.
Jako anegdotę dodam, że twórcom gry MMO : Eve Online udało się osiągnąć limit jaki stawia stos C. Wtedy w obroty wzięli odnogę Pyhona: Stackless, który nie zużywa stosu. Jako, że Python ma problemy ze skalowalnością horyzontalną (winowajca: GIL) ciekawi mnie jak rozwiązali sprawę komunikacji pomiędzy procesami działającymi na wielu rdzeniach.
Wracając do tematu. Konstrukcją Clojure, która nie powoduje przepełnień stosu jest loop - recur .
loop bierze wektor zmiennych tymczasowych podobnie do let, które są aktualizowane przez funkcję recur.
Rozkładając na czynniki pierwsze T1 mamy zainicjalizowaną zmienną x wartością 5 i sekwencję result jako pusty wektor: []. Idąc dalej mamy test x > 0. Jeżeli x będzie większe od 0 zostanie wywołana funkcja recur z nowym x pomniejszonym o jeden, a do pustego result zostanie dodany wynik powiększenia x o 2. Jeżeli warunek x > 0 nie będzie spełniony zostanie zwrócona aktualna wartość result. Czyli zaczynając od 5 do 1 włącznie zostanie zbudowany wektor [7 6 5 4 3].

  

T1: (= __
      (loop [x 5
             result []]
        (if (> x 0)
          (recur (dec x) (conj result (+ 2 x)))
          result)))

E1: [7 6 5 4 3]

Problem 25: Rearranging Code: ->>


Makro ->>. Służy do poprawy czytelności kodu. Jest to rozwinięta wersja ->, gdzie funkcje były jedno-parametrowe. Makro bierze dane z pierwszego argumentu i wstawia je na koniec listy podanej w kolejnym argumencie. Jeżeli są dodatkowe listy, to wynik poprzedniego działania jest wstawiany na koniec kolejnej. Na końcu tak zbudowana lista jest ewaluowana (wykonywana).
W zadaniu mamy wektor [2 5 4 1 3 6], który jest skracany o dwa elementy z przodu funkcją drop. Wynik to : [4 1 3 6] Następnie z wyniku brane są trzy pierwsze elementy : [4 1 3]. Kolejna rzecz, to za pomocą map zwiększenie elementów o 1, dając: [5 2 4].
Teraz trzeba znaleźć taką funkcję, która zrobi jakoś z tych danych 11. Okazuje się, że suma elementów to 11, więc użyję + wraz z funkcją operująca na listach.

T1: (= (__ (map inc (take 3 (drop 2 [2 5 4 1 3 6]))))
       (->> [2 5 4 1 3 6] (drop 2) (take 3) (map inc) (__))
       11)

E1: reduce +
E2: apply +

Problem 26: A nil key


Zadanie polega na znalezieniu funkcji, która pobierze dwa argumenty: klucz i mapę, oraz zwróci prawdę jeżeli klucz istnieje w tej mapie i ma wartość nil. Jest to dość ciekawe zadanie, bo mapa zwraca nil jeżeli klucz nie istnieje. Jak więc dowiedzieć się, że klucz istnieje i ma wartość nil?

Potrzebne będą dwa porównania. Jedno sprawdza, czy klucz istnieje za pomocą funkcji contains?, a drugie, czy wartość przypisana do klucza jest nil. Stąd rozwiązanie E1. Drugie rozwiązanie E2 jest lepsze, bo nie iteruje dwukrotnie po mapie tylko raz. W pętli loop biorę pierwszą parę klucz-dane z mapy i sprawdzam, czy nie dostaję nil. Dostanę nil jeżeli mapa będzie pusta. Jeżeli jest pusta zwracam falsz. Jeżeli nie sprawdzam, czy pierwszy element pary to mój klucz i przy okazji sprawdzam czy drugi to nil. Jeżeli to nasz klucz i ma wartość nil zwracam true. W przeciwnym wypadku biorę kolejny element z mapy oraz jej resztę i przekazuję jako nowe wartości do loop za pomocą recur.
Update:

Przykład E3 wykorzystuje możliwość przypisania do zmiennej lokalnej wartości domyślnej jeżeli klucz w mapie nie istnieje. Trzeba jednak uważać, by wartość domyślna nie mogła wystąpić w danych, bo dostaniemy niepoprawny wynik.

T1: (true?  (__ :a {:a nil :b 2}))
T2: (false? (__ :b {:a nil :b 2}))
T3: (false? (__ :c {:a nil :b 2}))

E1: #(and (contains? %2 %1) (nil? (%2 %1)))
E2: #(let [klucz %1 mapa %2]
        (loop [p (first mapa) r (rest mapa)]
                 (if (nil? p)
                     false
                     (if (and (= (first p) klucz) (nil? (second p)))
                             true
                             (recur (first r) (rest r))))))
E3: #(let [dane (%1 %2 :not_exist)]
        (if (nil? dane)
        true
        false))

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