09 września 2010

Zagadka z Clojure - funkcyjna niezmienność struktur danych

Logo ClojureKontynuuję moją przygodę z Clojure i zgodnie z planem wczytuję się w różnego rodzaju publikacje na ten temat - grupa dyskusyjna clojure na Google Groups monitorowana na bieżąco, na swoim koncie na twitterze podłączyłem się do co bardziej znaczących osób w światku Clojure i się wdrażam. Stopniowo i bez szaleństw, ale konsekwentnie. Ach i bym zapomniał - książkę "The Joy of Clojure" również czytam. Trochę wolniej niż zwykle - 1 rozdział na dzień podczas, gdy zwykle przychodzi mi przejść przez kilka rozdziałów, ale biorąc pod uwagę inne aktywności wokół Clojure, to i tak uważam, że utrzymuję dosyć wysokie tempo (wysoka samoocena wie, kiedy się ujawnić).

W trakcie lektury A monad tutorial for Clojure programmers (part 3) o monadach trafiłem na przykład, w którym zamiast idiomatycznego ('ma' w tym słowie ma niebagatelne znaczenie! :)) pobrania wartości dla danego klucza w mapie (klucz mapa) zastosowano (prawie) równoważną funkcję (get mapa klucz).
user=> (doc get)
-------------------------
clojure.core/get
([map key] [map key not-found])
  Returns the value mapped to key, not-found or nil if key not present.
nil
user=> (def m {:a \a})
#'user/m
user=> (class m)
clojure.lang.PersistentArrayMap
user=> (:a m)
\a
user=> (get m :a)
\a
user=> (= (get m :a) (:a m))
true
Różnica jest niewielka, acz istotna - obsługa sytuacji braku istnienia klucza w mapie. Warto pamiętać, że różnica istnieje, ale w przywołanym przykładzie użycie funkcji (get) sprowadza się do idiomatycznego użycia klucza do poszukiwania jego wartości w mapie. Innymi słowy, klucz w Clojure jest jednocześnie funkcją, która wyszukuje wartość dla niego samego. W przypadku (get) jest możliwość podania trzeciego parametru, który zostanie zwrócony, jeśli nie zostanie odszukany klucz.
user=> m
{:a \a}
user=> (:b m)
nil
user=> (get m :b \b)
\b
Jasne? Na pewno! I nie przyjmuję odpowiedzi, że nie :]

Tak przy okazji, właśnie znalazłem przykład do zobrazowania monady, która zareaguje na sytuację braku klucza! Poszukiwałem go przez dobrych kilka dni i właśnie znalazłem. Kolejny istotny powód dla publikowania swoich doświadczeń na blogu. Właśnie, gdyby ktoś rozważał bloga, ale brakowało mu inspiracji lub przewodnika, zgłaszam się na ochotnika, aby wskazać zainteresowanemu tematy. Najlepiej niech to będzie zapaleniec programowania funkcyjnego w dowolnej postaci - Haskell, Scala, Python, Lisp, Ruby, itp.

Wracając do tematu dzisiejszego wpisu, mam dla Ciebie zagadkę związaną z Clojure, aczkolwiek więcej tu logicznego myślenia niż samej znajomości języka. Warto jednak pamiętać, że wyróżnikiem programowania funkcyjnego jest przede wszystkim czystość funkcji - im większa tym lepiej. Zero skutków ubocznych i funkcja jedynie operuje na danych wejściowych zawsze zwracając pewną wartość, w szczególności nil. Pamiętaj, że do rozwiązania zagadki potrzebne jest zapamiętanie, że...patrz na tytuł wpisu. Pora na zagadkę, która mnie osobiście na zauważalny moment "zawiesiła": Dlaczego ostatnia linia zwróci "Nie ma elementu?!"? Nagrodą jest doświadczenie błogiego stanu nirwany zrozumienia podstawowej cechy programowania funkcyjnego.
user=> (def m {:a \a})
#'user/m
user=> m
{:a \a}
user=> (get :a a)
nil
user=> (get a :a)
\a
user=> (:b m)
nil
user=> (get m :b)
nil
user=> (get m :b "Nie ma elementu?!")
"Nie ma elementu?!"
user=> (doc assoc)
-------------------------
clojure.core/assoc
([map key val] [map key val & kvs])
  assoc[iate]. When applied to a map, returns a new map of the
    same (hashed/sorted) type, that contains the mapping of key(s) to
    val(s). When applied to a vector, returns a new vector that
    contains val at index. Note - index must be <= (count vector).
nil
user=> (assoc m :b \b)
{:b \b, :a \a}
user=> (get m :b "Nie ma elementu?!")
"Nie ma elementu?!"
Dlaczego ostatnia linia zwróci "Nie ma elementu?!"? Odpowiedzi dozwolone w komentarzach. Kto pierwszy, ten lepszy.

p.s. Jeśli komuś przypadła zagadka do gustu, proszony jest o wyrażenie swojego zachwytu w komentarzu, abym wiedział, czy kontynuować tego typu wpisy.

5 komentarzy:

  1. Bo mapy są immutable. (assoc m :b \b) nie zmieniło mapy m, tylko zwróciło nową mapę, więc w m dalej nie ma elementu.

    Btw, obsługa domyślnej wartości zwracanej to wcale nie jest ficzer get. Klucze i mapy używane jako funkcje biorą opcjonalny drugi argument:

    user> ({} :foo "Nie ma")
    "Nie ma"
    user> (:foo {} "Nie ma")
    "Nie ma"

    OdpowiedzUsuń
  2. Zagadki są jak najbardziej interesujące. Liczę na więcej :)

    OdpowiedzUsuń
  3. Zagadka prościutka, rozwiązanie już w tytule, czekamy na więcej :-)

    OdpowiedzUsuń
  4. To chyba nie jest jednak cecha lisp, że można zlekceważyć zwracaną wartość :P

    OdpowiedzUsuń
  5. Nie znam Lispa, więc nie potrafię na to odpowiedzieć. Tutaj liczyłbym na znawców tematu.

    Zwróciłeś mi Twoim komentarzem uwagę na ciekawe wyjaśnienie lekceważenia zwracanej wartości - jest wskazaniem, że zależy nam na skutku ubocznym, albo coś bardzo zbliżonego do niego (np. w monadach).

    OdpowiedzUsuń