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.