Jednym z bardziej charakterystycznych elementów, na które natyka się każdy, kto ma okazję pisać swoje pierwsze linijki w OCamlu są średniki, które występują w dwóch odmianach: zwykłej oraz podwójnej. Kiedy kompilator sieje błędami niekiedy można uspokoić go dopisując gdzieś w kodzie naszego programu podwójny średnik (lub zastępując takowym pojedynczy), ale czy jest to odpowiedni sposób rozwiązywania problemów? W tym wpisie opowiemy sobie jaki właściwie jest cel podwójnych i pojedynczych średników oraz wyjaśnimy kiedy należy (a kiedy nie należy) ich używać.
Spis treści
Średniki dwa ;;
Zacznijmy od średników podwójnych. Wpisując w top-levelu proste:
1 |
print_string "Hello world" |
musimy zakończyć tę linijkę dwoma następującymi po sobie średnikami (bez żadnych znaków pomiędzy). W przeciwnym wypadku interpreter nie podejmie wyzwania zinterpretowania naszego kodu. Podwójny średnik jest więc znakiem dla środowiska top-level, że wprowadzana przez nas definicja już się zakończyła i należy ją skompilować. Osobom, które korzystają z Pythona w trybie interaktywnym być może pomoże porównanie do sytuacji, w której chcąc skończyć definiowanie klasy lub funkcji po prostu wciskamy enter, tym samym kończąc serię linii rozpoczynających się od wcięć.
Trzeba przy tym wiedzieć, że nie musimy stawiać ;;
po każdej napisanej funkcji. Możemy zrobić to dopiero po zdefiniowaniu kilku funkcji, kiedy będziemy chcieli wywołać którąś z nich i zobaczyć rezultat tego wywołania. Obrazuje to poniższy przykład:
1 2 3 4 |
let add_one x = x + 1 let add_two x = add_one x + 1 let add_three x = add_two x |> add_one ;; |
Skoro zatem wiemy już jaką rolę pełni podwójny średnik w środowisku interaktywnym (top-level), to można postawić sobie pytanie o jego zastosowanie w normalnym kodzie programów OCamla. Załóżmy, że napisaliśmy sobie taki oto krótki programik, mający obliczać wartość funkcji dla skonkretyzowanego przez nas iksa i wyświetlać ją przy użyciu naszej funkcji print_number
:
1 2 3 4 5 6 7 8 9 |
let print_number n = let n = string_of_int n in let text = "Value: " ^ n ^ "\n" in print_string text let my_fun x = 3*x + 2 let y = my_fun 42 print_number y |
Zapisujemy pod nazwą program.ml
i wywołujemy kompilację ocamlc program.ml
. Naszym oczom ukazuje się komunikat błędu:
1 2 3 |
File "program.ml", line 8, characters 8-14: Error: This function has type int -> int It is applied to too many arguments; maybe you forgot a `;'. |
Hm, kompilator sugeruje nam, że zapomnieliśmy użyć średnika. Rzeczywiście, w naszym kodzie ani razu nie pojawił się ten znak. Niestety okazuje się, że nawet jeśli podążymy za tą sugestią, nie uda nam się skompilować programu. Zresztą sami spróbujcie powrzucać „zapomniany” średnik w różne miejsca – zapewniam, że wciąż będziecie napotykać błąd kompilacji. W tym momencie w głowie wielu początkujących programistów rodzi się pomysł poeksperymentowania z dwoma średnikami. I.. bingo! Poniższy kod poprawnie się skompiluje i uruchomi:
1 2 3 4 5 6 7 8 9 |
let print_number n = let n = string_of_int n in let text = "Value: " ^ n ^ "\n" in print_string text let my_fun x = 3*x + 2 let y = my_fun 42;; print_number y |
Czyżbyśmy zatem rozwiązali nasz problem? Dlaczego kompilator podpowiadał nam, żeby użyć pojedynczego średnika? Program wprawdzie działa, ale z pewnością nie jest przykładem dobrego stylu. Dobra zasada brzmi bowiem: nigdy nie używaj ;;
w kodzie OCamla (poza trybem interaktywnym i dyrektywami interpretera)
Jak więc zmodyfikować powyższy kod, aby działał, a przy tym nie ranił oczu wytrawnych programistów OCamla? Możemy pozbyć się wszelkich średników, a ostatnią linijkę zastąpić taką:
1 |
let _ = print_number y |
lub taką:
1 |
let () = print_number y |
O co w tym chodzi? Wywołanie funkcji print_number
wbrew pozorom też zwraca pewną wartość. Wartością tą jest unit
, czyli coś na kształt void
z języka C – oznaczenie, że funkcja zwraca „pustą” wartość. W OCamlu jako języku funkcyjnym główny nacisk położony jest na wartościowanie, czyli wyliczanie wartości funkcji, a wywoływanie funkcji dla tzw. efektów ubocznych powinno być raczej rzadkością. Ponieważ jednak OCaml nie jest językiem czysto funkcyjnym pozwala na pewne ustępstwa, takie jak właśnie bezpośrednie wywołanie jakiejś funkcji tylko po to, aby wydrukowała ona coś na ekran. W takim przypadku można powiedzieć kompilatorowi „wiem, że ta funkcja coś zwraca, ale nie interesuje mnie jej wartość”, pisząc let _ =
. W przypadku funkcji zwracających typ unit
ładniejszym i bezpieczniejszym rozwiązaniem jest bezpośrednie poinformowanie kompilatora o tym fakcie i poproszenie go, aby sprawdził, czy rzeczywiście zwracana wartość to zawsze unit
. Taką rolę pełni wyrażenie let () =
które matchuje wartość po prawej stronie znaku równa się z typem unit
.
Kończąc temat podwójnych średników (które skutecznie wyeliminowaliśmy z naszego kodu), należy jeszcze zwrócić uwagę na pewną historyczną zaszłość. Otóż w języku Caml Light, z którego wywodzi się OCaml, podwójne średniki były obowiązkowym oznaczeniem końca wyrażenia. Z tego też powodu można spotkać się z nimi w starszych fragmentach kodu, które były przepisywane z Caml Light.
Średnik samotny ;
Trochę wyżej wspomniałem już o funkcjach, które wywołuje się dla ich skutków ubocznych (takich jak wyświetlanie tekstu na ekranie), a nie dla zwracanej przez nich wartości. Z użyciem takich funkcji wiąże się w OCamlu zastosowanie pojedynczego średnika. Nie jest on, jak w wielu innych językach, znakiem zakończenia instrukcji, ale separatorem wyrażeń. Działanie tego operatora jest następujące: ewaluuje pierwsze wyrażenie, później wylicza wartość drugiego wyrażenia, a na końcu zwraca ją. Jeśli wartością pierwszego z wyrażeń będzie coś innego niż unit
, to kompilator wyprodukuje ostrzeżenie. Poniżej kilka przykładów użycia pojedynczego średnika:
1 2 3 4 5 6 7 8 |
let print_hello_and_return_number n = print_string "Hello "; print_string "world\n"; n let () = () ; () let x = print_string "" ; 5 let _ = print_hello_and_return_number x |
Kiedy zatem używać średnika, a kiedy let () =
? W pewnych sytuacjach jest to jedynie kwestia gustu. Dla przykładu naszą funkcję print_hello_and_return_number
można równie dobrze napisać w ten sposób i będzie ona działać dokładnie tak samo:
1 2 3 4 |
let print_hello_and_return_number n = let () = print_string "Hello " in let () = print_string "world\n" in n |
Istnieją jednak przypadki, w których należy zachować większą ostrożność. Rozważmy jeden z nich:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let x = 10 let f x = let text = "f(x) called with x=" ^ string_of_int x in print_endline text let () = if x < 3 then print_endline "X < 3"; f x let () = if x < 3 then let () = print_endline "X < 3" in f x |
Czy funkcja f
zostanie chociaż raz wywołana, skoro wartość x
wynosi 10? Niestety tak (w linii nr 9), a przecież wydaje się to być sprzeczne z intuicją piszącego taki kod programisty. Winna okazuje się tu precedencja operatorów, która dla separatora ;
jest zbyt słaba, aby „skleić się” z wywołaniem f x
wewnątrz instrukcji warunkowej.
Ceplusplusowy przecinek
A na sam koniec, w ramach dostrzegania międzyjęzykowych powiązań, proponuję fragment kodu w C++, prezentujący ten sam koncept, co działanie operatora ;
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int foo(int x) { std::cout << "foo(" << x << ")\n"; return 2;} int bar(int x) { return x*2; } int main() { int n = 7; int val = (foo(n), bar(n)); std::cout << "Value=" << val << std::endl; } |