12 października 2010

Funkcje vs stałe - funkcyjne przypisanie

Niektóre proste rzeczy przychodzą z pewnym opóźnieniem i nawet jeśli wydają się być albo wręcz są proste, trzeba czasu, aby dotarło to do odpowiednich miejsc w mojej korze mózgowej. I później są kwiatki! Sprawa rozbija się o nawyk poprawnego czytania składni Clojure, którego jeszcze nie nabyłem, ale każdy dzień sprawia, że czuję, że jestem bliżej.

Zastanawiało mnie ostatnio, dlaczego taka konstrukcja - forma w Clojure:
(defn- year-now [] (. (DateTime.) getYear))
miałaby być lepsza od tej:
(def- year-now (. (DateTime.) getYear))
Rozwiązanie przyszło, kiedy zamieniłem obie konstrukcje na odpowiedniki w Javie - pierwsze to po prostu deklaracja prywatnej funkcji (przez makro defn-), a drugie to stała, również prywatna. Kluczem jest wystąpienie owych nawiasów klamrowych, które są miejscem deklarowania "imiennych" parametrów. W pierwszym przypadku, funkcji, każdorazowe wywołanie da inną wartość, podczas gdy w drugim otrzymamy tę samą. Możnaby powiedzieć, że funkcja year-now nie jest funkcją czystą, bo różne jej wywołania mogą skutkować dwiema różnymi odpowiedziami. Nie będzie to zbyt odkrywcze, jeśli napiszę, że jest to nielada problem nie tylko dla programistów funkcyjnych, ale i imperatywno-obiektowych, gdzie bez znajomości implementacji przetestowanie działania funkcji jest nietrywialne.

Kolejnym "opóźnieniem" w moim mentalnym rozwoju było zrozumienie działania przypisania
(def a 1)
do...funkcji! Gdybym czytał o tym wczoraj, zapewne byłbym pierwszym, który zapytałby "Jak to?!" Czyż dowolna aplikacja nie jest po prostu jedną wielką funkcją?! W Javie mamy statyczną metodę main(String..), która definiuje punkt startowy i tylko w ten sposób polecenie java z podaną klasą na linii poleceń wie jak ją uruchomić. Taki jest kontrakt.

Gdyby się temu przyjrzeć, to zadeklarowanie stałej final int a = 5 w Javie, tak na prawdę nie różni się niczym od przekazania parametru wejściowego funkcji o nazwie a, np. void metoda(int a) { // użycie a jak stałej }, w której a występuje na tych samych zasadach, co zadeklarowanie jej jako stałej bezpośrednio w ciele. Proste, nieprawdaż? I tak, podczas lektury A monad tutorial for Clojure programmers (part 1) doznałem wspaniałego efektu Aha! i zrozumiałem tak oczywistą oczywistość.

W Clojure poniższe konstrukcje są sobie (prawie) równoważne.
(def a 1)

(let [a 1] a)

((fn [a] a) 1)
Czy w ogóle istnieje jakakolwiek różnica? Różnica jest i to znacząca. Przypisanie wartości do stałej jest jednorazowe, w miejscu jego wystąpienia (w Javie możemy odłożyć ten krok, aż do wykonania konstruktora), podczas gdy zbudowanie funkcji w roli przypisania (opakowanie przypisania funkcją) pozwala na dynamiczne przypisanie w dowolnym momencie wywołania jej z innymi wartościami. Możnaby założyć, że przypisanie jest "lukrem", który zamieniany jest podczas kompilacji na wykonanie właściwej funkcji z pojedynczym parametrem. Jeśli funkcja jest bytem pierwszej kategorii, to nie ma w tym nic odkrywczego.

Załóżmy, że mamy taką sekwencję wyrażeń (wszystkie równoważne):
(def a 1)
(println a)

(let [a 1]
  (println a))

(def f 
  (fn [a] a))
(f 1)
Jedynie ostatnie "przypisanie" możemy wykonywać wielokrotnie, każdorazowo podając inną wartość na wejściu. Pierwszą i drugą konstrukcję (formę w Clojure) możemy zmodyfikować w kodzie, przesłaniając poprzednie wystąpienie, ale tylko ostatnią konstrukcję funkcyjną nazwałbym w pełni samoistną i wielokrotnego użytku.

To musi mieć wpływ na nasz warsztat imperatywno-obiektowy w Javie. Zauważam jednak pewien problem - brak wsparcia dla funkcji jako bytów pierwszej kategorii w Javie, więc nie ma mowy o ich przekazywaniu do funkcji wyższego poziomu (takich, które akceptują wejście z funkcją). Tutaj właśnie widziałbym rolę Clojure jako języka wspierającego nasze programowanie w Javie - do pisania zamkniętych procedur.

Gdzieś znalazłem takie stwierdzenie, że początkowo programujemy z konstrukcjami prostymi - przekazywanie wartości do funkcji, kolejnym krokiem jest budowanie funkcji wyższego poziomu, aby w kolejnym kroku szukać...monad. Był jeszcze jeden poziom, ale nie mogę sobie go przypomnieć. Gdyby przełożyć to na język kaski, to jeszcze nie wiem, czy i w ogóle można na tej wiedzy zarobić, ale zarobić *się* można ucząc się tego wszystkiego bez praktycznego wykorzystania :)