Wpis z mikrobloga

#programowanie #learnclojurewithmikroblog #clojure

W dzisiejszym odcinku - wstęp do struktur danych i funkcji wyższego rzędu. Najczęściej używane struktury danych w clojure to:

[1 2 3 4] ; wektor
{


"Jan Nowak"


1984, 


"Anna Kowalska"


1999} ; mapa
#{ 


"a"





"o"





"e"





"i"





"u"


} ; zbiór
Struktury te mogą być niejednorodne (np. w 1 wektorze mogą być elementy różnych typów) i dowolnie zagnieżdżone (np. kluczami mogą być dowolne typy).

[ []
[1]
[1 2 3]
[[[]]] ]
{ [


"Jan"





"Nowak"


] {


"płeć"





"M"


,



"rocznik"


1984},
[


"Anna"





"Kowalska"


] {


"płeć"





"K"


,



"rocznik"


1999} }
#{ [3 4 5]
[5 4 3]
[3 3 4] }
Co ciekawe - porównanie działa poprawnie dla zagnieżdżonych struktur danych:

(



```**```
[ []
``````
     [1]
``````
     [1 2 3] ]
``````
   [ []
``````
     [1]
``````
     [1 2 (
```**```

```**```
1 1 1)] ] ) ; true
``````
(
```**```

```**```
[ []
``````
     [1]
``````
     [1 2 4] ]
``````
   [ []
``````
     [1]
``````
     [1 2 (
```**```

```**```
1 1 1)] ] ) ; false
```Podstawowe operacje (conj assoc get):conj dodaje element do struktury danych (miejsce, gdzie element zostanie dodany zależy od typu struktury danych - dla wektorów jest to koniec, dla map i zbiorów kolejność nie jest wyspecyfikowane). Przykład:```
(conj [1 2 3 4] 5) ; zwróci [1 2 3 4 5]

(conj {"a" 1 "b" 2} ["c" 3]) ; zwróci {"c" 3, "a" 1, "b" 2} (przecinki są traktowane jak spacje - używa się ich tylko dla czytelności)

(conj #{"ala" "ula" "ewa"} "janek") ; zwróci #{"ula" "ewa" "ala" "janek"}

(conj #{"ala" "ula" "ewa"} "ula") ; zwróci #{"ula" "ewa" "ala"}

```Zauważmy, że clojure jako język funkcyjny nie modyfikuje wartości, tylko zawsze tworzy NOWE wartości, a stare pozostają bez zmian. Przykład:```
(def xs [1 2 3 4 5])

xs ; zwróci [1 2 3 4 5]

(conj xs 6) ; zwróci [1 2 3 4 5 6], ale nie zmieni wartości xs

xs ; zwróci dalej [1 2 3 4 5]

```assoc podstawia pod wartość pod klucz. Dla wektorów klucz musi być liczbą naturalną (indeksem od 0 do n),dla map może być dowolną wartością. ```
(assoc ["a" "b" "b" "d"] 2 "c") ; zwraca ["a" "b" "c" "d"]

(assoc {"a" 1, "b" 2} "c" 3) ; zwraca {"c" 3, "a" 1, "b" 2}

(assoc {"a" 1, "b" 2} "b" 3) ; zwraca {"a" 1, "b" 3}

(dissoc {"a" 1, "b" 2} "b") ; zwraca {"a" 1}

```A co, jeśli chcemy pobrać wartość z kolekcji? Możemy to zrobić na kilka sposobów. Po pierwsze można użyć funkcji get```
(get ["a" "b" "c"] 1) ; zwraca "b"

(get ["a" "b" "c"] 4) ; zwraca nil - specjalna wartość oznaczająca brak w kolekcji

(get {"a" 1, "b" 2} "b") ; zwraca 2

(get #{"a" "b"} "b") ; zwraca "b"

(get #{"a" "b"} "c") ; zwraca nil bo zbiór nie zawiera "c"

```Możemy również skorzystać z tego, że kolekcje można traktować jak funkcję indeks->wartość. Czyli można je wywoływać.```
({"USD" "dolar", "EUR" "euro", "PLN" "złoty"} "EUR") ; zwróci "euro"

(["a" "b" "c"] 1) ; zwróci "b"

```Jest bardzo dożo funkcji operujących na strukturach danych, po pozostałe odsyłam do dokumentacji [http://clojure.org/data_structures](http://clojure.org/data_structures) .Ok, coś tam wiemy o strukturach danych. Powiedzmy, teraz, że chcemy przejrzeć listę. W języku imperatywnym napisalibyśmy fora lub for-eacha. W języku funkcyjnym możemy użyć rekurencji, jednak jest to dużo kodu. Dla najczęściej powtarzających się typów pętli możemy stworzyć abstrakcję - funkcję, która wykona pętlę zamiast nas, a w jej wnętrzu wykona dostarczony przez nas kod w odpowiednim miejscu. Taka funkcja musi otrzymać od nas funkcję jako argument - nazywa się to funkcja wyższego rzędu i jest podstawą funkcyjnego stylu programowania. Przykład:```
(map (fn [x] (* x x)) [1 2 3 4]) ; zwróci (1 4 9 16)

```Funkcja map dostaje funkcję n-argumentową i n kolekcji. Map w pętli pobiera po 1 elemencie z każdej kolekcji i wykonuje funkcję dla pobranych elementów, wynik funkcji jest wstawiany do sekwencji, która jest zwracana przez map. Przykład dla funkcji 2-argumentowej:```
(map + [1 2 3 4] [2 2 3 2]) ; zwróci (3 4 6 6) czyli po prostu kolekcję sum odpowiadających sobie elementów z 2 wektorów źródłowych

```Kolejną funkcją wyższego rzędu jest filter. Używamy go do filtrowania kolekcji zadanym predykatem. Predykat to funkcja jednoargumentowa zwracająca boolean. Zwyczajowo w clojure predykaty mają na końcu nazwy znak zapytania. Np odd? sprawdza, czy argument jest nieparzysty. Przykład :```
(filter odd? [1 2 3 4 5 6 7]) ; zwróci (1 3 5 7)

(filter (fn [x] (> x 3)) [1 2 3 4 5 6 7]) ; zwróci (4 5 6 7)

```Ostatnią (i najogólniejszą) funkcją wyższego rzędu, którą dziś pokrótce omówimy jest reduce. Reduce służy do zredukowania kolekcji do 1 wartości. Można ją używać do sumowania, wybierania minimum/maksimum, ale także do zupełnie innych celów. Prawie każdą operację da się zapisać jako kombinację kilku map i reduce. Przykład:```
(reduce + [1 2 3 4]) ; zwróci 10

```Reduce bierze funkcję 2-argumentową i kolekcję. Wykonuje funkcję dla 2 pierwszych elementów,potem dla wyniku z poprzedniego kroku i kolejnego elementu, aż zużyje całą kolekcję. Inny przykład to maksimum:```
(reduce (fn [x y] (if (>= x y) x y)) [10 20 5 11 100 0]) ; zwróci 100

```Reduce ma kilka bajerów - po pierwsze można podać wartość początkową (i wtedy reduce zadziała również dla pustej kolekcji).```
(reduce (fn [x y] (if (>= x y) x y)) -1000 []) ; zwróci -1000

(reduce (fn [x y] (if (>= x y) x y)) -1000 [10 20 5 11 100 0]) ; zwróci 100

```Po drugie jeśli podana funkcja ma również wersję 0-argumentową to dla pustej kolekcji reduce zwróci wynik wywołania funkcji bez argumentów, jeśli nie podamy reduce wartości początkowej.```
(reduce * []) ; zwróci 1 bo (*) zwraca 1

(reduce * [1 2 3 4]) ; zwróci 24

```Z tych przykładów może się błędnie wydawać, że wynikiem reduce musi być wartość tego samego typu, co elementy kolekcji, albo, że funkcja przekazywana do reduce musi przyjmować argumenty tego samego typu. To NIEPRAWDA, reduce jest dużo ogólniejsze. Funkcja przekazywana do reduce może przyjmować argumenty różnego typu i wtedy reduce może robić prawie wszystko. Przykład - implementacja funkcji podobnej do filter przy użyciu reduce:```
(
```**```
defn 
```**```
nasz-filter [f coll]
``````
          (
```**```
reduce 
```**```
(
```**```
fn 
```**```
[result-so-far next-el]
``````
                      (
```**```
if 
```**```
(f next-el)
``````
                          (
```**```
conj 
```**```
result-so-far next-el)
``````
                          result-so-far))
``````
                  []
``````
                  coll))
``````
(nasz-filter (
```**```
fn 
```**```
[x] (
```**```

```**```
x 3)) [1 2 3 4 5]) ; zwróci [4 5]
```Zadanie 2. Napiszmy małą bibliotekę geometryczną - operacje na dwuwymiarowych wektorach. Wektor matematyczny zapisujmy jako [x y]. ```
(defn v2d+ [v1 v2] ...) ; suma wektorow v1 i v2

(defn v2d- [v1 v2] ...) ; roznica wektorow v1 i v2

(defn v2d*s [v s] ...) ; iloczyn wektor razy skalar (skalowanie wektora)

(defn v2d*v2d [v1 v2] ...) ; iloczyn skalarny wektorów

(defn v2d-len [v] ...) ; długość wektora (pierwiastek liczymy istniejącą funkcją (Math/sqrt x))

Podpowiedź: spróbujcie zapisać jedne z tych funkcji jako kombinację innych.

Zadanie 2*. Spróbujcie dostosować powyższe funkcje do działania na wektorach 2- i 3-wymiarowych. Może niektóre z nich od razu działają dla 3 wymiarów? Jeśli nie - może da się je tak napisać?
  • 5
@tell_me_more: formatowanie kodu działa spoko, dzięki @dzien_dobry .

Nie wiem, czy nie przesadzam z ilością materiału na raz, ale chciałem dojść do fajnych zadań. Pamiętam, że jak próbowałem sam napisać taką biblioteczkę do wektorów w stylu funkcyjnym, to mi w głowie coś "zaskoczyło" i potem już o wielu problemach można myśleć wysokopoziomowo jako kombinacja map, reduce i filter. Jestem ciekaw waszych wrażeń.

Odpowiedź na poprzednie Zadanie 1 (nieoptymalna, bo o tail-call