14 października 2012

Różne miejsca wystąpienia => w języku Scala

Podczas zajęć "Functional Programming Principles in Scala" dowiedziałem się o możliwości definiowania parametrów wejściowych funkcji typu by-name, których wartość wyliczana jest z opóźnieniem - na czas, kiedy ich wartość jest potrzebna i z naciskiem na możliwe skutki uboczne. Zapis takiego parametru składa się ze znaku implikacji => oraz nazwy parametru funkcji. Jak rozumiem ze zdania "This feature must be applied with care; a caller expecting by-value semantics will be surprised." znajomość tej konstrukcji należy do tych rzadko stosowanych i sugeruje znajomość Scali na wyższym poziomie, a mimo to pojawiło się na początkowej lekcji "Functional Programming Principles in Scala" i dodatkowo było przedmiotem ćwiczenia (!) Widać, że Odersky'iemu zależy na znajomości tej konstrukcji i trzeba mieć się na baczności.

Przyjrzyj się takiemu zapisowi funkcji i pomyśl, jaki będzie efekt wykonania jej.
scala> def f(y: Int, x: => Int) = y
f: (y: Int, x: => Int)Int

scala> f(5, 1000^100000000)
res2: Int = 5
Jeśli dobrze rozumiem materiał, to wykonanie f powinno być równie efektywne co wykonanie f(5, 1), czyli skoro drugi argument funkcji jest wyliczany na czas użycia, a nie jest użyty w powyższym przykładzie, to do obliczenia wartości w ogóle nie dojdzie.

Na wykładzie o funkcjach wyższego rzędu pojawiła się notacja parametru wejściowego funkcji, który jest również funkcją i tutaj ponownie pojawił się symbol implikacji =>. To był ten moment, w którym kolejny raz naszło mnie na rozmyślanie o stałych jako swego rodzaju funkcji, które wyglądają jak funkcje stałe, ale różnią się tym, że nie są literałami funkcyjnymi a prymitywami.
scala> def g(h: () => Int) = h()
g: (h: () => Int)Int

scala> g(() => 5)
res8: Int = 5
Sądzę, że zrozumienie różnicy między stałymi a funkcjami stałymi ma niebagatelne znaczenie w poznawaniu zachowania języka wspierającego konstrukcje funkcyjne. W znanych mi językach - Java, Clojure, Scala - 5 jest zawsze 5 i "wykonanie" jej nie pozostawia śladu, podczas gdy wykonanie funkcji stałej zwracającej 5 może pozostawić swój ślad w środowisku.

A skąd mnie naszło na rozprawianie o tym?

Porównajmy zapis deklaracji argumentu wejściowego po nazwie x: => Int od deklaracji argumentu będącego funkcją f: Int => Int. W pierwszym przypadku definiujemy parametr wyliczany z opóźnieniem, w drugim podobnie, ale mamy dla tego specjalną nazwę - funkcja. Funkcja to obliczenie, które będzie wykonane na czas jego wykonania. Bardzo podobnie do owego opóźnionego wyliczania dla x: => Int. Jak zaznaczono w już wspominanym rozdziale Call by name w Effective Scala użycie funkcji do celów modelowania opóźnienia wykonania jest zalecane jako jawne wskazanie opóźnienia.

Zatem foruje się podejście oparte na funkcji.
scala> def f(y: Int, x: () => Int) = y
f: (y: Int, x: () => Int)Int

scala> f(5, () => 1000^100000000)
res9: Int = 5
Różnica niewielka, a jakie konsekwencje!

Ot, taka ciekawostka (para)naukowa, której objawienia mogłem doświadczyć podczas analizowania wykładu.

6 komentarzy:

  1. Sugeruję stosować radę "be applied with care" do tego typu zaleceń z effective scala; no daj spokój, deklaracja sposobu przekazania wartości parametru metody ma być znajomością Scali "na wyższym poziomie"? A podwójna strzałka w prawo występuje jeszcze w innych miejscach w Scali. Grzegorz Balcerek

    OdpowiedzUsuń
    Odpowiedzi
    1. Dla mnie użycie opóźnienia w wyliczaniu wartości jest równoznaczne z optymalizacją, której na początku mojej przygody ze Scalą pewnie nie potrzebuję. Coś mi mówi, że pewnie długo jeszcze nie będę potrzebował. Stąd twierdzę, że to znajomość na wyższym poziomie.

      Możesz opisać przykład, który uzasadniałby zastosowanie tego opóźnienia? Gdzie udało Ci się z tego skorzystać? Gdzie widziałeś zastosowanie? Odpowiedzi na te pytania mogą znacząco usprawnić ocenę przydatności =>. Nie sądzisz?

      Usuń
  2. Ok. Jednak "pewnie długo jeszcze nie będę potrzebował" a "znajomość na wyższym poziomie" to trochę inaczej brzmi, nie uważasz? Sposoby ewaluacji parametrów (przez wartość, przez nazwę) to jednak raczej podstawy informatyki, nie sądzisz?
    Możesz się z tym spotkać choćby korzystając z metody Option.getOrElse (zauważ dzielenie przez zero i brak wyjątku):

    scala> Some(1).getOrElse(1/0)
    res0: Int = 1

    Inne przykłady sobie możesz podejrzeć choćby w mojej książce na stronach 43, 87-89, 92, 145, 147-152, 402-407. Miłej nauki. Grzegorz

    OdpowiedzUsuń
    Odpowiedzi
    1. Dla mnie to to samo - nie potrzebuję == wyższy poziom (znajomości tematu). Różnimy się po prostu w tym co uważamy za podstawy informatyki. Czyż to nie sprawia, że w ogóle dyskutujemy? Gdybyśmy się zgodzili już na początku, to o czym rozmawialibyśmy? :) Miłe.

      Na pewno się z nimi zapoznam i pozwolę na komentarz.

      Usuń
  3. Skoro tak stawiasz sprawę, to pozwalam sobie krótko uzasadnić. Otóż do podstaw informatyki zaliczam rachunek lambda. W szczególności jako podstawę programowania funkcyjnego. Natomiast ewaluacja parametrów przez wartość i przez nazwę ma bezpośredni związek ze strategiami redukcji termów rachunku lambda do postaci normalnej. Grzegorz

    OdpowiedzUsuń
    Odpowiedzi
    1. Moje przydługie doświadczenie naukowe na UMK i MIMUW wskazuje, że rachunek lambda (pewnie jak topologia czy algebra uniwersalna) nie jest specjalnie wałkowany na studiach, więc nie oczekiwałbym obszernej wiedzy w społeczeństwie. Uważam, że to jeden z powodów, dla których nie dostrzega się piękna programowania funkcyjnego.

      Może najwyższa pora to zmienić przez praktyczne wprowadzenie do "produktów" teorii matematycznej, np. do języków funkcyjnych? Wciąż znajduję o tym mało w polskiej i zagranicznej blogosferze.

      Usuń