13 listopada 2009

Funkcyjne wyjście z REPL z (exit) i ankieta językowa

Już dawno nie czułem takiej radości z tak niewielkiej ilości kodu. Chyba wciąż jestem przerażony liczbą nawiasów w Clojure, więc kiedy napisałem funkcję (exit) bezbłędnie za pierwszym razem, można sobie wyobrazić, jaki byłem szczęśliwy.
 user=> (def exit (fn [] (. System exit 0)))
#'user/exit
user=> (exit)
Brakowało mi tej funkcji w Clojure REPL, bo ^C, czy ^Z + ENTER pachniały mi jakąś niedoróbką. W tak niewielkiej funkcji udało mi się wykorzystać 3 istotne możliwości Clojure - definiowanie zmiennej przez def, której przypisano funkcję anonimową przez fn, która z kolei wykorzystuje integrację Clojure z Javą za pomocą specjalnej formy . (kropka). Ale ze mnie programista, co? :)

Można to jednak lekko podrasować i wykorzystać specjalną formę #(), która również definiuje funkcję anonimową. Owa funkcja anonimowa w niektórych kręgach nazywana jest...domknięciem (ang. closure). Domknięcie w Clojure nie różni się bardzo w porównaniu do tradycyjnego wywołania funkcji, ale tym razem owe wywołanie fn kończy się stworzeniem nowej. Funkcja, która operuje na innych, w tym zwraca je, nazywamy funkcją wyższego poziomu (ang. higher-order function).

Wracając do naszego #() to możnaby powyższe zapisać tak:
 user=> (def exit #(. System exit 0))
#'user/exit
user=> (exit)
Przeglądając kod źródłowy "Game of Life" Scotta Frasera trafiłem na metodę java.lang.Runtime.availableProcessors(), o której istnieniu nie wiedziałem. W zasadzie o wielu metodach jeszcze nie wiem i wiele z nich wywrze na mnie podobne, pozytywne wrażenie, aczkolwiek ta ma tą zaletę, że utwierdziła mnie w przekonaniu, że mój laptop faktycznie posiada procesor 2-rdzeniowy i 2 wątki powinny działać równolegle (biorąc pod uwagę działanie wszystkich innych programów w tle, pewnie tak nie będzie i jeden z wątków i tak będzie szybszy).
 user=> (def available-procs (.. java.lang.Runtime getRuntime availableProcessors))
#'user/available-procs
user=> (println available-procs)
2
nil
Zadanie dla wytrwałych: Jak zmienić def powyżej, aby możliwe było wykonanie (available-procs), który zwróci 2, tj.
 user=> (available-procs)
2
p.s. Rozpocząłem kolejną ankietę odnośnie języków programowania "Jaki język programowania używasz poza Javą?" (jest na blogu po prawej u góry). Nie ma znaczenia, czy do zabawy, w projekcie, własnej ciekawości, czytasz o nim książkę, czy podobnie. Poświęcasz swój czas, więc wystarczy, aby określić go jako "używasz". Interesuje mnie, czy chociażby rozważasz alternatywę dla Javy i jaki to jest język. Wszystkie języki poza JVM wpadły do kategorii "Inne (poza JVM)". Głosujcie!

11 komentarzy:

  1. Odp.:

    (def available-procs #(.. java.lang.Runtime getRuntime availableProcessors))

    OdpowiedzUsuń
  2. Dokładnie tak! Właśnie sobie uzmysłowiłem, więc zadanie uzupełniające: Proszę o wersję krótszą (aż o 10 znaków!) :)

    OdpowiedzUsuń
  3. java.lang jest domyślnie zaimportowany więc nie trzeba pisać pełnej nazwy Runtime.

    Stąd też wynika, że wygodnie można tworzyć typy obudowane np. (Integer. "123")

    OdpowiedzUsuń
  4. Oczywiście, że Scala :). Czasem jeszcze Python, gdyż studia tego wymagają.

    OdpowiedzUsuń
  5. Ten komentarz został usunięty przez autora.

    OdpowiedzUsuń
  6. wlaśnie te nawiasy mnie odpychają, z funkcyjnych języków to preferuje Erlanga z kilku powodów:

    1. bo ma OTP i natywne wsparcie do "clustrów" bez cudów typu gridgain czy teracota
    2. bo radzi sobie z większymi obciążeniami niż Java
    3. bo jest prosty jak budowa cepa (pare słów kluczowych) a przy tym o wiele czytelniejszy niż wiekszość czysto funkcyjnych języków
    4. bo ma wspaniały matching (zarówno na poziomie wyrażeń jaki i funkcji).

    co do Clojure, to podobny jest problem jak ze Scalą ... mieszanie kodu funkcyjnego i OO, powoduje że i tak nie wykorzystujemy w pełni FP, kod nie jest pozbawiony side effect bo korzysta bibliotek napisanych w javie. Jak najbardziej funkcyjne konstrukcje w Scali uwielbiam (dodatkowo traits, case clasess + aktorów) podobnie jak w Pythonie i w Rubym, ale moim zdaniem JVM nie nadaje się do czysto funkcyjnego programowania ... bo nie został do tego zaprojektowany.

    OdpowiedzUsuń
  7. Mam pytanie do Andrzeja. Czy wykorzystywałeś erlang w komercyjnych rozwiązaniach, czy jest to język, który po prostu chciałeś poznać?

    OdpowiedzUsuń
  8. jest jakiś powód, dla którego używasz def zamiast defn?

    OdpowiedzUsuń
  9. @Daniel, super pytanie, które mnie na chwilę wstrzymało zanim znalazłem odpowiedź. def definiuje zmienną globalną (powiedzmy stałą w sensie Javy), a defn definiuje funkcję. Ja chciałem zdefiniować funkcję (exit), więc mogłem wybrać oba podejścia. Po prostu wygodniej mi było z

    (def exit #(. System exit 0))

    zamiast

    (defn exit [] (. System exit 0))

    Jest krócej o znak. Nie wiem, czy są inne zalety jednego względem drugiego. W pierwszym definiuję domknięcie - funkcję anonimową - a w drugim funkcję nieanonimową. Na chwilę obecną powiedziałbym, że to kwestia gustu.

    OdpowiedzUsuń
  10. Ważne jest żeby zrozumieć, że te dwie wersje są równoważne (defn jest makrem, które rozwija się do def; spróbuj zrobić na obu macroexpand-1). Druga jest "kanoniczna" w tym sensie, że przekazuje intencję zdefiniowania nazwanej funkcji (tzn. jak czytasz taki kod, to widzisz od razu, że exit jest czymś, co można wołać). A jeszcze bardziej kanonicznie jest korzystać z cukru składniowego do wołania metod statycznych Javy:

    (defn exit [] (System/exit))

    Pozwolę sobie też sprecyzować różnicę między funkcją anonimową a domknięciem, bo to nie to samo. W Clojure każda funkcja, nawet nazwana, jest (może być) domknięciem; za to istnieją języki, w których ani funkcje anonimowe ani zwykłe nie są pełnoprawnymi domknięciami - OIDP stare wersje Pythona tak miały.

    Żeby zrozumieć różnicę, rozważmy następujący kawałek kodu:

    (let [tmp (atom 0)]
    (defn counter []
    (swap! tmp inc)))

    Forma let wprowadza tu wiązanie symbolu tmp do pewnej wartości, widoczne *wyłącznie* w ciele tej formy; nie ma sposobu, aby dobrać się do niego z zewnątrz. (Pełna enkapsulacja!) A w ciele let mamy definicję funkcji i w jej obrębie możemy używać tmp. Mówi się, że ta funkcja "domyka się" nad środowiskiem, w którym tmp dowiązane jest do pewnego atomu. (Zahacza to o Clojurowe mechanizmy współbieżności, ale nie trzeba ich znać, żeby zrozumieć to o czym piszę).

    W ten sposób zdefiniowaliśmy licznik:

    (counter)
    1

    (counter)
    2

    (counter)
    3

    OdpowiedzUsuń
  11. Errata: miało być oczywiście:

    (defn exit [] (System/exit 0))

    OdpowiedzUsuń