Dla osób przybywających ze świata programowania imperatywnego zobaczenie jak wyglądają typy, które OCaml dedukuje dla definiowanych przez programistę funkcji, często rodzi pewne pytania i niejasności. Zawierająca strzałki notacja na pierwszy rzut oka nie wydaje się najbardziej oczywistym sposobem na wyrażenie np. faktu, że funkcja przyjmuje trzy argumenty typu int
i zwraca zmienną tego samego typu. Spójrzmy na poniższy przykład kodu wykonanego w top-levelu (aby zobaczyć typy dla funkcji zdefiniowanych w pliku można posłużyć się komendą ocamlc -i
, która wyświetli wydedukowane interfejsy):
1 2 |
# let add a b c = a + b + c;; val add : int -> int -> int -> int = <fun> |
W takim zapisie w żaden sposób nie jest wyróżniony zwracany typ (oprócz tego, że znajduje się na ostatniej pozycji). Okazuje się, że ma to swoje uzasadnienie, a jest nim currying (po polsku znany jako rozwijanie funkcji). Pojęcie to pochodzi od nazwiska amerykańskiego matematyka Haskella Curry’ego (a zgadnijcie jaki język zaczerpnął swą nazwę od jego imienia) i można wytłumaczyć je w ten sposób – mając funkcję przyjmującą N argumentów możemy rozwinąć ją w ciąg N funkcji, z których każda pobiera tylko jeden argument. Oto dwa równoznaczne wywołania stworzonej przez nas funkcji:
1 2 3 4 |
# add 1 2 3;; - : int = 6 # ((add 1) 2) 3;; - : int = 6 |
Na sygnaturę funkcji add
można zatem równie dobrze spojrzeć w ten sposób:
1 |
val add : int -> (int -> (int -> int)) = <fun> |
Wychodzi wtedy na to, że zwracanym typem nie jest int
, ale kolejna funkcja (która również zwraca funkcję i dopiero ta ostatnia funkcja zwraca liczbę). A skoro add
da się potraktować jako funkcję jednoargumentową, to całkiem legalnie można ją wywołać w ten sposób:
1 2 |
# add 1;; - : int -> int -> int = <fun> |
Jaki jest rezultat tego wywołania? Kolejna funkcja! W tym momencie zbliżamy się do praktycznego zastosowania curryingu, czyli tzw. częściowej aplikacji (ang. partial application). Pojęcie to niekiedy mylnie utożsamiane jest z rozwijaniem funkcji, jednak jego znaczenie, choć powiązane, jest odmienne. Odnosi się ono do sytuacji, w której część argumentów funkcji jest bindowana, przez co funkcja z N-argumentowej staje się funkcją M-argumentową, gdzie M < N. W praktyce wygląda to tak:
1 2 3 4 5 6 7 8 9 10 |
# let add_two = add 2;; (* częściowa aplikacja jednego z trzech argumentów *) val add_two : int -> int -> int = <fun> # let add_six = add 3 3;; (* częściowa aplikacja dwóch argumentów *) val add_six : int -> int = <fun> # add_six 1;; - : int = 7 # add_two 1 2;; - : int = 5 # (add_two 3) 5;; (* rozwinięcie funkcji *) - : int = 10 |
Poniżej zestawienie, systematyzujące poznane fakty i pokazujące jak wygląda typ naszej funkcji add
w zależności od liczby podanych doń argumentów:
1 2 3 4 |
add : int -> int -> int -> int add 1 : int -> int -> int add 1 2 : int -> int add 1 2 3 : int |
Nie wiem jak dla Was, ale dla mnie powyższy przykład jest wystarczającym uzasadnieniem dziwnej strzałkowej notacji sygnatur funkcji, występującej w OCamlu. Nie tylko zresztą w OCamlu, bo z podobnym widokiem spotkamy się też w paru innych językach funkcyjnych, nie tylko tych z rodziny ML. Z zaprezentowanego w tym wpisie kodu można przy okazji wywnioskować co nieco o łączności operatorów: strzałka (->
) jest łączna prawostronnie (bo int -> int -> int
jest tym samym co int -> (int -> int)
), zaś aplikacja funkcji łączna lewostronnie (gdyż add 1 2
równoznaczne jest z (add 1) 2
).