OCaml/Wersja do druku
Spis treści
[edytuj]- Wstęp
- O podręczniku
- Wybór języka - Dlaczego OCaml?
- Konfiguracja środowiska pracy.
- Najważniejsze cechy języka.
- Objective Caml
- Pierwsze kroki w OCamlu
- Funkcje jako wartości
- Rekurencja i iteracje
- Struktury danych w języku
- Listy i polimorfia
- Krotki
- Tablice
- Typy wariacyjne i dopasowywanie
- Rekordy i zmienne
- Pierwszy program
- Mechanizmy języka
- Wyjątki
- Nazwane i opcjonalne argumenty funkcji
O podręczniku
[edytuj]Charakterystyka podręcznika
[edytuj]Tekst ma charakter wprowadzenia do języka OCaml dla osób, które wcześniej pisały w innym języku - prawdopodobnie imperatywnym, ewentualnie obiektowym jak C/C++. Po jego przeczytaniu czytelnik powinien potrafić pisać własne programy z pomocą opisu modułów języka oraz w bezstresowy sposób pogłębiać dalej swoją wiedzę.
Autorzy
[edytuj]
Dlaczego OCaml?
[edytuj]Każdy chyba programista, który od wielu lat programował w językach imperatywnych i obiektowych jak C/C++, PHP, Ruby, zastanawiał się jak zbudowane są języki funkcyjne i czym od nich się różnią. W końcu -- w C i C++ są przecież funkcje?
Przed rozpoczęciem nauki Camla warto zrobić mały przegląd istniejących języków tego typu (Lisp, Haskell, ...) Każdy z nich ma swój komplet zalet. Każdy musi podjąć tę decyzję sam, ale może mu w tym pomóc podsumowanie zalet języka OCaml:
- Języki z rodziny ML (Szczególnie często używanymi są Standard ML oraz OCaml) są jednymi z najszybszych języków (w rankingach często w top3). OCaml można kompilować do przenośnego kodu bajtowego (ang. bytecodu) tak jak np. Java, do szybkich natywnych binarek (jak C), a także można w nim pisać skrypty, które są kompilowane przed uruchomieniem.
- Umożliwia programowanie funkcyjne, obiektowe i imperatywne.
- Jest językiem silnie typowanym (silniej niż C), co ma tak wielu zwolenników co przeciwników. W każdym razie na pewno zmniejsza ilość błędów popełnianych podczas pisania programu oraz umożliwia o wiele doskonalszą optymalizację kodu.
- Pomimo powyższego punktu OCaml jest zaopatrzony w inferencję typów. Potrafi dedukować z jakich typów chce korzystać programista.
- Zawiera bardzo efektywny garbage collector.
- Ma dość miłą składnię, choć dla programisty, który nigdy w języku funkcyjnym nie pisał z pewnością na początku będzie obca.
- Miła informacja dla osób wielbiących platformę .NET. Język F# rozwijany przez Microsoft Research jest bezpośrednio oparty o Objective Camla i jest z nim kompatybilny.
Konfiguracja środowiska pracy
[edytuj]Używam systemu GNU/Linux i większość komentarzy będę odnosił do tego systemu jednak pod innymi systemami wiele zagadnień będzie wyglądało dokładnie tak samo.
OCamla możemy ściągnąć ze strony głównej projektu http://caml.inria.fr/. Znajdziemy tam również więcej dokumentacji, opis modułów itp. Większość dystrybucji GNU/Linuxa posiada w repozytoriach swoich pakietów odpowiednie paczki i jego ręczne ściąganie nie jest konieczne.
Po instalacji powinniśmy móc korzystać z paru aplikacji. W szczególności interesujące będą dla nas:
- ocamlc -- kompilator do bytecodu. Kod bajtowy ocamla uruchamia inny program o nazwie ocamlrun. W systemach unixowych jest to uproszczone przez automatyczne dodanie linii #!/usr/bin/ocamlrun do tworzonych plików.
- ocamlopt -- kompilator generujący natywne pliki wykonywalne.
- ocaml -- wygodne środowisko działające na zasadzie wczytaj-wykonaj-wyświetl. Bardzo wygodne do nauki i szybkiego próbowania fragmentów kodu .
Większość nauki radzę przeprowadzić sobie za pomocą trzeciego programu, który jednak posiada pewną wadę. Ze względu na przenośność ma bardzo uproszczony interfejs i w oryginalnej wersji nie posiada ani historii komend, ani możliwości poprawienia wcześniej wpisanego tekstu bez kasowania reszty.
Przynajmniej w Linuxie znalazłem parę rozwiązań tego problemu. Możemy np. skorzystać z wrapperów biblioteki readline. Znam dwa "rlwrap" i "ledit". Pierwszy nie działał mi dobrze, więc ostatecznie do mojego .bashrc wrzuciłem linijkę:
alias ocaml="ledit ocaml"
Co rozwiązało moje problemy ze środowiskiem "ocaml". W razie czego zalecam spróbowanie obu wrapperów.
Istnieje też bardziej eleganckie, choć też bardziej skomplikowane rozwiązanie polegające na zainstalowaniu tuareg-mode do edytora Emacs. Prócz podświetlania składni, wcinania kodu udostępnia on też możliwość interaktywnego uruchomienia "ocaml" z historią komend i podświetlaniem składni na żywo.
Najważniejsze cechy języka
[edytuj]Caml jest językiem statycznie typowanym, co oznacza tyle, że każda użyta w nim nazwa (funkcji, zmiennej, argumentu funkcji) ma określony ścisły typ, który podczas działania programu nie ulega zmianie. Ponadto programista nie musi (prawie nigdy) definiować typów używanych wyrażeń. W takim razie, można by spytać -- jak to działa? Zmienne mają określone typy, ale nikt nigdzie nie stwierdza jakie to są typy? Tak, dokładnie tak to wygląda.
Caml w trakcie kompilacji przeprowadza tzw. "type inferring", na podstawie sposobów w jaki używano zmiennej ustala jej typ. To wydawać się może bardzo proste, ale działa również dla typów złożonych, takich jak listy, rekordy, listy rekordów, obiekty. Działa również pomiędzy osobnymi plikami, bibliotekami Camla i modułami. Technika ta pozwala na uniknięcie wielu częstych błędów pojawiających się podczas pisania programów w językach bez silnego typowania (np. w PHP), w związku z tym przyspiesza tworzenie programów (również zwalniając programistę z obowiązku deklarowania zmiennych), przyspiesza wykonywanie programu przez dokładniejsze przeprowadzenie optymalizacji. Wprowadza ona też też jednak pewne niedogodności.
Jak i w wielu innych językach, mamy do dyspozycji zestaw podstawowych typów, jakimi są: int, float, char, string, bool oraz typ unit (oznaczany często dwoma nawiasami "()"), będący odpowiednikiem void z języka C. Podobnie jak w wielu innych językach pojedyncze znaki (typu char) otoczone są pojedynczym cudzysłowem, np. 'a', ciągi znaków podwójnym. Z typów podstawowych można zbudować inne typy takie jak listy, krotki (ang. tuples), rekordy, tablice, typy wariacyjne.
Aby móc rozróżnić pomiędzy typami całkowitymi i zmiennoprzecinkowymi w Camlu programista używa dwóch zestawów symboli dla operacji arytmetycznych. Jest to "+ - * /" dla liczb całkowitych (typ int), oraz te same symbole z dodaną do nich kropką: "+. -. *. /." dla operacji na typach float. W Camlu w przeciwieństwie do C nie istnieje tzw. "implicit casting", czyli automatyczne rzutowanie (konwersja) typów z jednego na drugi. Aby w C skonwertować integer na float, użylibyśmy zwykłego przypisania
float a; int b = 10;
a = b;
Co najwyżej możemy ręcznie uczynić to rzutowaniem jawnym (explicit cast), dodając:
a = (float) b;
Wielu programistów może nie zdawać sobie sprawy z tego, że konwersja liczby całkowitej na zmiennoprzecinkową nie jest taką całkiem tanią operacją. W tym języku wszystkie rzutowania są przeprowadzane jawnie. W standardowym module (który jest zawsze załączony do programu i nazywa się "Pervasives") dostępny jest szereg funkcji w postaci: string_of_int (konwertuje liczbę na ciąg znaków), int_of_string (w przeciwnym kierunku), int_of_float, float_of_int, itp.
Język funkcyjny charakteryzuje się tym, że funkcje są w nim obywatelami pierwszej klasy ("first-class citizens"). Można je w dowolnym miejscu tworzyć, przekazywać jak dowolne inne dane do innych funkcji. Poza tym każde wyrażenie używane w języku (np. warunki if) posiada swój typ.
To tyle jeśli chodzi o obowiązkowy wstęp. Resztę wiedzy dużo przyjemniej będzie sobie przyswoić poprzez przykłady.
Pierwsze kroki w OCamlu
[edytuj]Jeśli mamy przygotowane środowisko programistyczne do nauki, możemy przerobić parę prostych programów. Uruchamiamy "ocaml" i wpisujemy pierwsze banalne przykłady:
$ ocaml
Objective Caml version 3.09.3
# 5 + 5;;
- : int = 10
# 6.3 *. 2.5;;
- : float = 15.75
# 5 > 6;;
- : bool = false
# 'a';;
- : char = 'a'
# "abc";;
- : string = "abc"
# ();;
- : unit = ()
Fragmenty kodu są wpisane za znakiem zachęty '#' i w tym przypadku jest to pojedyncze wyrażenie "5 + 5", za którym znajduje się separator ";;". Caml używa podwójnego średnika do odseparowywania wyrażeń na najwyższym "poziomie" kodu (ang. top-level). Znak ten w wielu przypadkach się pomija, a wewnątrz funkcji do odseparowywania poszczególnych instrukcji używa się pojedynczego średnika.
Ocaml w odpowiedzi na nasz pierwszy przykład zwrócił taką informację: "- : int = 10", która zawiera wynikowy (ang. inferred) typ wyrażenia (int) oraz jego wartość (10). Łącznik oznacza, że wyrażeniu nie nadaliśmy żadnej nazwy. Gdyby jednak chcieć je jakoś nazwać?
# let a = 5 + 5;;
val a : int = 10
Ha! Teraz posiadamy "zmienną" o nazwie "a" i wartości 10. Ważny jest tutaj cudzysłów dookoła słowa "zmienna". Tak na prawdę powyższe wyrażenie nie jest taką zmienną jaką znamy z języków imperatywnych (czyli np. z C). Jest to tylko nazwane "globalne wyrażenie". Wiąże się to z pewnymi istotnymi cechami. Nie możemy dla przykładu zmienić wartości "a" bez stworzenia nowej etykiety (choćby o tej samej nazwie, ale co jak łatwo się przekonać jest czymś zupełnie innym). Caml zwraca uwagę na wielkość liter; wyrażenia/etykiety zaczynające się z dużej litery są zarezerwowane dla nazw modułów i np. konstruktorów typów wariacyjnych (o czym się przekonamy!).
W bardzo podobny sposób możemy w Camlu stworzyć funkcje:
# let add a b = a + b;;
val add : int -> int -> int = <fun>
# add 5 6;;
- : int = 11
# let a = 10;;
val a : int = 10
# add 5 (a * 5 + 5);;
- : int = 60
# let addfive = add 5;;
val addfive : int -> int = <fun>
# addfive 40;;
- : int = 45
# let add2 a b =
let sum = a + b in
sum
;;
val add2 : int -> int -> int = <fun>
Tym razem wydedukowany typ wyrażenia z linii pierwszej jest trochę bardziej złożony: "int -> int -> int = <fun>". Oznacza on ni mniej ni więcej tylko funkcję, która przyjmuje dwa inty i zwraca int.
W linii czwartej widać sposób w jaki wywołujemy funkcje – nie używamy nawiasów wokół argumentów i nie oddzielamy ich przecinkami tak jak to ma miejsce w C. Nawiasów użyjemy w następnym przypadku, gdy będziemy chcieli podać jako argument funkcji wynik innego wyrażenia. Jeśliby ich tam zbrakło Ocaml wywołałby funkcję z parametrami 5 i a, po czym pomnożył wynik przez 5 i dodał 5.
Okrągłe nawiasy w Camlu spełniają podobną funkcję co klamry w C – odgradzają bloki kodu grupując wyrażenia. Zauważmy więc, że te operacje arytmetyczne w nawiasie, stanowią pewne wyrażenie, któremu podczas wartościowania nadany zostaje typ int o konkretnej wartości. W wielu przypadkach zamiast nawiasów można użyć również słów begin i end jeśli zwiększają one czytelność. Nieraz wyglądało by to jednak po prostu śmiesznie:
# add 5 begin a*5+5 end;;
- : int = 60
Wracając do naszego poprzedniego sporego przykładu; w linii 13 możemy zauważyć coś dziwnego. Wywołaliśmy naszą dwuargumentową funkcję tylko z jednym argumentem. I co? Błędu nie ma! Co więcej, wyrażenie zwróciło nam "coś" co zapisaliśmy pod etykietką addfive. Z wydedukowanego typu "int -> int = <fun>", widzimy, że add, wywołane z jednym argumentem zwróciło funkcję, która przyjmuje jeden argument typu int, i zwraca jednego inta!
Co więcej, jak się później okazało nasza nowa funkcja dodaje do podanego argumentu piątkę. Niesamowite! Co lepsze, ta charakterystyczna dla języków funkcyjnych cecha ma pewne praktyczne zastosowania. Przykładowo możemy stworzyć ogólną funkcję, z której następnie przez podanie paru argumentów stworzymy drugą pochodną funkcję. Możemy ją z kolei podać do innej funkcji, która przy jakiejś okazji ją wywoła. Uruchamianie funkcji bez podanych wszystkich argumentów nazywać będziemy częściową aplikacją/częściowym aplikowaniem (ang. partial application).
Ostatni przykład ilustruje sposób w jaki tworzy się lokalne wyrażenia, czyli np. etykiety lub funkcje wewnątrz innych funkcji. Używamy bardzo podobnego wyrażenia do let a = b;;
, tylko zakończonego słowem "in" zamiast
średników.
Funkcje jako wartości
[edytuj]Przed nami rozdział opisujący główne cechy, które Caml zawdzięcza swoim funkcyjnym korzeniom. Ich dokładnie rozumienie często nie jest niezbędne do tworzenia prostych programów, ale z pewnością może to ułatwić.
(* Tworzymy funkcję pobierającą 3 argumenty *)
# let add_three a b c = a + b + c;;
val add_three : int -> int -> int -> int = <fun>
(* Jeśli wywołamy ją z mniejszą liczbą argumentów, zwróci
* nam nową funkcję "odejmując" z typu jedno "int ->"
* z lewej strony *)
# add_three 1;;
- : int -> int -> int = <fun>
# add_three 1 2;;
- : int -> int = <fun>
# add_three 1 2 3;;
- : int = 6
(* Może więc przy zwykłym wywoływaniu
* dzieje się tak samo? *)
# (((add_three 1) 2) 3);;
- : int = 6
Proces sprowadzania funkcji wieloargumentowych do jednoargumentowych w matematyce i informatyce (rachunku lambda) nazywa się "currying". Możemy też stworzyć funkcję z paru funkcji jednoargumentowych:
# let add_three = fun a -> fun b -> fun c -> a + b + c;;
val add_three : int -> int -> int -> int = <fun>
Nie znam wprawdzie bezpośredniego zastosowania takiej metody deklarowania funkcji, lecz słowa kluczowego "fun" (lub "function") używa się dość często. Wtedy kiedy chcemy podkreślić, że typem zwracanym przez funkcję jest inna funkcja, jeśli chcemy stworzyć "funkcję anonimową", lub gdy chcemy stworzyć funkcję, która ma przeprowadzać dopasowywanie (o tym za chwilkę).
# let deriv f dx = fun x -> ( f(x +. dx) -. f(x) ) /. dx;;
val deriv : (float -> float)->float->float->float = <fun>
# let sin' = deriv sin 1e-6;;
val sin' : float -> float = <fun>
# sin' 3.141592653589793;;
- : float = -1.00000000013961143
W tym przykładzie stworzyliśmy funkcję, która dla dowolnej podanej funkcji typu float -> float zwraca funkcję (float -> float), która oblicza przybliżenie jej matematycznej pochodnej. Definicja funkcji deriv w tym przykładzie używała słowa "fun". Możemy zapisać ją bez użycia tego słowa kluczowego w sposób absolutnie równoważny:
# let deriv f dx x = ( f(x +. dx) -. f(x) ) /. dx;;
val deriv : (float -> float)->float->float->float = <fun>
Wydaje mi się, że ta metoda mniej wskazuje na sposób w jaki funkcja powinna być wywoływana. Wcześniej miała 2 argumenty i zwracała funkcję. Teraz ma 3 i zwraca wartość pochodnej w punkcie... chyba, że zastosujemy częściową aplikację.
Można również prosto zdefiniować funkcję służącą do składania funkcji:
# let compose f g = function x -> f(g(x));;
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>
# let sqrt_cos = compose sqrt cos;;
val sqrt_cos : float -> float = <fun>
Znane nam już operacje dodawania, odejmowania, mnożenia i dzielenia to również zwykłe dwuargumentowe funkcje zdefiniowane w module Pervasives. Programista bardzo łatwo może definiować swoje własne operatory. Właściwie tak jak w typowo obiektowych językach (Ruby), gdzie wszystko jest obiektem tutaj praktycznie wszystko jest funkcją.
Rekurencja i iteracje
[edytuj]W językach funkcyjnych bardzo naturalną techniką jest rekurencja. Tworząc funkcje rekurencyjne dodajemy do definicji słówko "rec", bez niego funkcja nie będzie mogła wywoływać samej siebie. Dla przykładu prostą pętlę można rozwiązać w następujący sposób:
# let rec loop i =
print_endline "Looping!";
if (i>1) then loop (i-1)
;;
val loop : int -> unit = <fun>
# loop 3;;
Looping!
Looping!
Looping!
- : unit = ()
Funkcje "tail-recursive" to taki przypadek rekurencyjnych funkcji, w których ponowne wejście do funkcji znajduje się na ich końcu. Caml optymalizuje wszystkie takie przypadki generując assembler lub kod bajtowy w postaci normalnej iteracji. Dlatego stosowanie rekurencji do zapętlania programu nie jest obciążone żadnym większym zużyciem pamięci ani czasem potrzebnym na wywołanie funkcji jakby to miało miejsce w języku C i podobnych.
Istnieją również implementacje pętli for i while, lecz zazwyczaj się z nich nie korzysta. W Camlu nie istnieją wyrażenia break i continue. Jeśli jednak zajdzie potrzeba przerwania rekurencji lub pętli używany jest wyjątek Exit (zdefiniowany w Pervasives). W modułach zdefiniowane jest również wiele "iteratorów" -- funkcji służących do wykonywania cyklicznych operacji na np. elementach list, które opiszemy później.
Rekurencji ciąg dalszy:
# let rec silnia n =
if n = 0 then 1 else n * silnia (n - 1);;
# let rec fib n =
if (n <= 2) then
1
else fib (n - 1) + fib (n - 2);;
val fib : int -> int = <fun>
Pierwsza funkcja, jak każdy się pewnie domyślił, pozwala obliczyć silnię z n. Druga zwraca n-ty wyraz ciągu Fibonacciego (licząc od zera). Widzimy tu, wiele opisanych wcześniej elementów. Ostatnie wyrażenie w "bloku kodu" zwraca wartość, oraz że każdy blok posiada jakiś typ. Jeśli np. w else zechcielibyśmy zwrócić typ float (zamiast "1" umieszczając tam "1.0") otrzymamy informację, że używane przez nas wyrażenie o typie float, jest używane jako int.
Funkcja fib nie jest funkcją "tail-recursive", dlatego możemy ją efektywniej napisać stosując funkcję pomocniczą, stworzoną za pomocą konstrukcji let ... in
# let fib x =
(* Funkcja pomocnicza *)
let rec loop a b i =
if i <= 2 then
b
else let next = a + b in
loop b next (i-1)
in
loop 1 1 x (* Jej wywołanie *)
;;
val fib : int -> int = <fun>
(* Wspomniana pętla for. Wyświetla początek
* ciągu fibonacciego: *)
# for i = 1 to 10 do
print_int (fib i);
print_newline ();
done;;
1
1
2
3
5
8
(...)
- : unit = ()
(* Wyrażenie w pętli "for" musi zwracać typ unit. *)
Listy i polimorfia
[edytuj]Listy zbudowane są z wielu elementów tego samego typu. Pustą listę oznacza się symbolem [], natomiast operator :: doczepia na początku listy nowy element. Można je tworzyć na dwa sposoby; rekurencyjnie -- doczepiając do pustej listy kolejne elementy, lub w równoważny sposób -- podając w kwadratowym nawiasie elementy oddzielone średnikami. Spójrzmy na poniższy przykład:
# [1;2;3;4;5];;
- : int list = [1; 2; 3; 4; 5]
# 1 :: 2 :: 4 :: 5 :: [];;
- : int list = [1; 2; 4; 5]
# ['a'; 'b'; 'c'; 'd'];;
- : char list = ['a'; 'b'; 'c'; 'd']
# [];;
- : 'a list = []
Typem ogólnym listy jest 'a list
, gdzie 'a jest
"typem polimorficznym", który oznacza "dowolny typ". Szczególnymi przypadkami
list może być lista intów: int list
lub znaków: char
list
. Do łączenia dwóch list w jedną używany jest operator '@'.
Jeśli OCaml nie będzie w stanie określić typu któregoś z argumentów funkcji, stworzy funkcję polimorficzną, która może przyjąć argument o dowolnym typie. Wiele z funkcji z biblioteki standardowej jest właśnie tak zbudowana. Dla przykładu rozważmy funkcję List.map:
# List.map;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>
# List.map (fun a -> a + 5) [1; 2; 3];;
- : int list = [6; 7; 8]
Funkcja ta przyjmuje dwa argumenty: Funkcję ('a -> 'b)
przyjmującą jeden
argument dowolnego typu, i zwracającą argument innego typu (lub tego samego,
ale niekoniecznie) oraz 'a list
, czyli listę tego samego typu, który
przyjmuje funkcja i zwraca listę elementów ('b list)
zwracanych przez
funkcję. Trochę to było skomplikowane ale wybrnęliśmy.
W tym przykładzie również możemy zauważyć zastosowanie funkcji anonimowej:
(fun a-> a+5)
, która została stworzona specjalnie na potrzeby wywołania List.map --
nie posiada własnej nazwy i nie może być później użyta ponownie.
Krotki
[edytuj]Krotki (ang. tuples) to typ danych, który składa się z kilku elementów o tym samym lub różnym typie. Są to odpowiednio pary, trójki, czwórki etc. Do oddzielenia ich poszczególnych elementów używane są przecinki (w przeciwieństwie do średników używanych w listach). Dla zwiększenia czytelności często otacza się je nawiasami. W opisie typów pojawia się zamiast przecinka symbol gwiazdki '*'.
Mogą być używane do zwracania dwóch zmiennych przez funkcje, do definiowania list par elementów i w wielu innych zastosowaniach. Przykładowo:
# let func a = (a, "tekst");;
val func : 'a -> 'a * string = <fun>
(* Funkcja przyjmuje dowolny parametr; zwraca krotkę. *)
# func 5;;
- : int * string = (5, "tekst")
# let one, two = func "inny";;
val one : string = "inny"
val two : string = "tekst"
# (4, 5.0) :: (10, 12.32) :: [];;
- : (int * float) list = [(4, 5.); (10, 12.32)]
Warto zauważyć, że w przeciwieństwie do list, w krotkach do oddzielenia kolejnych wyrażeń używa się przecinka (a nie średnika). Kod:
# [1, 2, 3, 4];;
- : (int * int * int * int) list = [(1, 2, 3, 4)]
Jak widać nie tworzy listy liczb, ale listę zawierającą krotki czterech liczb.
Tablice
[edytuj]Tablice są przykładem imperatywnych elementów w języku OCaml. Można by zapytać: „Co w nich takiego imperatywnego?” Większość typów w Camlu jest niezmienna (ang. immutable). Dla przykładu listy. Jeśli raz stworzymy listę i potem zachcemy zmienić w niej jeden element musimy całą listę zbudować od nowa zmieniając jeden element. Właściwie podobnie jest z etykietami. Tablice natomiast można dowolnie zmieniać. Można także w ten sam sposób postąpić z rekordami (używając słowa kluczowego mutable).
# let arr = [| "Hello"; "World" |];;
val arr : string array = [|"Hello"; "World"|]
# arr.(0);;
- : string = "Hello"
# arr.(0) <- "test";;
- : unit = ()
# arr.(1).[4];;
- : char = 'd'
# arr.(1).[4] <- 'X';;
- : unit = ()
# arr;;
- : string array = [|"test"; "WorlX"|]
W powyższym przykładzie w linii pierwszej stworzona została tablica z przypisaną jej nazwą "arr". Następnie odwołaliśmy się do pierwszego elementu tablicy (o indeksie zero) i zmieniliśmy jego wartość na ciąg "test".
W linii 10, odwołaliśmy się do pojedynczego znaku ciągu. Jak widać robi się to z wykorzystaniem zapisu: ciąg.(indeks), czyli używając nawiasu okrągłego -- nie kwadratowego jak to się dzieje w przypadku tablic. Następnie zmieniliśmy ów znak ciągu na inny. Ciągi znaków też mają imperatywną charakterystykę w Camlu -- jeśli ktoś potrzebuje czysto funkcyjne ciągi tworzy je jako listę znaków. Są trochę wolniejsze o tych wbudowanych natywnie język. W module Pervasives jest zdefiniowany operator '^' -- funkcja, która pobiera dwa ciągi i zwraca jeden, będący sklejeniem ciągów składowych.
Typy wariacyjne i dopasowywanie
[edytuj]Na pewno spotkaliście się kiedyś w innych językach z podobną konstrukcją:
struct Dane {
enum {TYPE_INT, TYPE_DOUBLE, TYPE_STRING} Type;
union {
int integer;
double num;
char *str;
} u;
}
Zamierzenie jest jasne: Chcemy mieć typ, który będzie czasem ciągiem, czasem liczbą całkowitą, a w jeszcze innych przypadkach liczbą zmiennoprzecinkową. Jednak taka implementacja jaką widzimy powyżej ma szereg wad. Prócz tego, że wygląda skomplikowanie (a wyglądała by jeszcze gorzej jakby zamiast enum użyć #define) programista może się pomylić. Wpisać do unii liczbę, a ustawić, że znajduje się tam para liczb.
Analizę takiego typu najpewniej przeprowadzimy w jakimś rozbudowanym wyrażeniu typu "switch", gdzie sprawdzimy wszystkie (lub nie wszystkie — kolejny błąd) przypadki i wykonamy określone akcje. Caml udostępnia programiście dla takich zastosowań bardzo potężny i często wykorzystywany typ wariacyjny, wraz z narzędziem jakim jest dopasowywanie (ang. matching). Ale zacznijmy od początku:
(* Deklarujemy typ wariacyjny *)
# type colors = Green | Blue | Red | Yellow | Pink;;
type colors = Green | Blue | Red | Yellow | Pink
(* Wywołujemy któryś z jego konstruktorów. *)
# Green;;
- : colors = Green
(* Mała dopasowująca funkcja *)
# let setColor color =
match color with
| Green -> print_endline "Zielony!"
| Blue -> print_endline "Niebieski!"
| Yellow -> print_endline "Żółty!"
| _ -> print_endline "Inny kolor?"
;;
val setColor : colors -> unit = <fun>
# setColor Green;;
Zielony!
- : unit = ()
# setColor Pink;;
Inny kolor?
- : unit = ()
W przykładzie zdefiniowaliśmy typ wariacyjny o nazwie colors. Może on posiadać 5 "wartości" odpowiadających odpowiednim kolorom. Te wartości/etykiety nazywane są konstruktorami typu — będziemy ich używać gdy będziemy chcieli stworzyć wyrażenie o tym typie.
Następnie stworzyliśmy funkcję setColor; przyjmuje ona jeden argument, będący kolorem. Dopasowuje go i następnie wykonuje związaną z nim akcję. Podkreślenie zostało użyte do dopasowania wszystkich pozostałych kolorów. Kompilator nie pozwoliłby nam na stworzenie dopasowania, które pomijało by niektóre przypadki.
Funkcje, których jedynym zadaniem jest dopasowanie jednego lub więcej
argumentów można zapisywać w krótszej formie. Nie opisuje się
wtedy nazw funkcji i zamiast zapisu match [argument] with ...
używamy słowa "function". Powyższy przykład był praktycznie zwykłym enum wraz
z ładnie wyglądającym "switch", rozszerzmy go więc:
# type colors = Green | Yellow | RGB of (int * int * int);;
type colors = Green | Yellow | RGB of (int * int * int)
# let color = RGB (120, 15, 166);;
val color : colors = RGB (120, 15, 166)
# let setColor = function
| Green -> print_endline "Zielony?"
| RGB (r,g,b) ->
printf "Wybrałeś kolor %d %d %d\n" r g b
| _ -> print_endline "Inny kolorek"
;;
val setColor : colors -> unit = <fun>
# setColor color;;
Wybrałeś kolor RGB 120 15 166
- : unit = ()
Teraz widać, że typ wariacyjny nie tylko pozwala na określenie nazw paru elementów, ale także pozwala im przypisać wartość dowolnego typu. W przykładzie jest to akurat krotka składająca się z 3 intów, które opisują kolor za pomocą mieszanki czerwonego, zielonego i niebieskiego. Podczas dopasowywania możemy ją sobie od razu wygodnie "rozbić" na 3 dowolnie nazwane przez nas etykiety. Do funkcji printf użytej w przykładzie wrócimy jeszcze później.
Dopasowywanie pozwala sprecyzować dodatkowe warunki, które muszą zachodzić by "wzór" pasował, np.:
match color with
| Green -> print_endline "Zielony?"
| RGB (r,g,b) when r=0 && b=0 ->
print_endline "Wciąż zielony!"
| RGB (r,g,b) ->
printf "Wybrałeś kolor RGB %d %d %d\n" r g b
| _ -> print_endline "Inny kolorek"
;;
Możemy użyć dopasowywania do jeszcze innego zdefiniowania funkcji obliczającej ciąg fibonacciego. Tym razem definicja małpuje bezpośrednio definicję matematyczną:
let rec fib = function
1 -> 1
| 2 -> 1
| x -> fib (x - 1) + fib (x - 2)
Dopasowywanie działa więc nie tylko z typem wariacyjnym. Może nawet używać wyrażeń regularnych! W każdym przypadku można pomijać pierwszy znak '|', choć ze względów estetycznych zwykle go używam.
Dopasowywaniem można również bardzo wygodnie przeglądać listy:
# let data = [1; 2; 3; 4; 5; 99];;
val data : int list = [1; 2; 3; 4; 5; 99]
# let rec add_to_list lst num =
match lst with
| [] -> []
| hd :: tl -> (hd + num) :: add_to_list tl num
;;
val add_to_list : int list -> int -> int list = <fun>
# add_to_list data 5;;
- : int list = [6; 7; 8; 9; 10; 104]
Jak to działa? Funkcja add_to_list przegląda listę od lewej do prawej
składając nową listę. Pierwszy przypadek dopasowania jest banalny: jeśli
dopasowujemy pustą listę, zwróć pustą listę. W drugim przypadku wzorzec
dopasowania hd :: tl
dzieli daną nam niepustą listę na jej
pierwszy element (hd) oraz ogon (wszystkie pozostałe). Do głowy dodajemy
num
i doczepiamy za nim wynik rekursywnego przetwarzania dla
ogona. "hd" i "tl" to są dowolne używane przez nas nazwy.
Wielkość tego Wprowadzenia nie pozwala na dokładnie omówienie wszystkich zastosowań, przedstawię wam jedynie jeszcze parę przykładów, bez dokładnego opisu
Typy wariacyjne mogą być definiowane rekurencyjnie! Możemy np. zdefiniować sobie drzewo binarne, które posiada w swoich liściach liczby całkowite:
# type drzewo = Lisc of int | Drzewo of (drzewo * drzewo);;
type drzewo = Lisc of int | Drzewo of (drzewo * drzewo)
(* I przykład małego drzewka:
/\
/\ 10
/\ 11
5 10
*)
# Drzewo (Drzewo (Lisc 5, Lisc 10), Lisc 11), (Lisc 10);;
Jeśli chcemy opisać tylko strukturę drzewa, które ma posiadać liście o różnych typach możemy posłużyć się polimorfią. Dla tak zdefiniowanego typu danych, możemy tworzyć polimorficzne funkcje (oparte np. o dopasowywanie):
# type 'a drzewo =
Lisc of 'a
| Drzewo of ('a drzewo * 'a drzewo);;
# Lisc 1.0;;
- : float drzewo = Lisc 1.
# Lisc 1;;
- : int drzewo = Lisc 1
Nie mówiąc już o tym, że prócz zwykłych "typów wariacyjnych" istnieją również tzw. typy wariacyjne polimorficzne (ang. polymorphic variants), które zachowują się trochę inaczej. Przedstawię tutaj tylko króciutki przykładzik. W małych programach rzadko jest potrzeba sięgania po ten typ danych.
# let func a = match a with
| (`DATA a) -> printf "liczba: %d\n" a
| (`STRDATA s) -> printf "ciag: %s\n" s;;
val func : [< `DATA of int | `STRDATA of string ]
-> unit = <fun>
# func (`DATA 4);;
liczba:4
- : unit = ()
# func (`STRDATA "test");;
ciag: test
- : unit = ()
Charakteryzują się użyciem symbolu "`" przed nazwą dla odróżnienia od zwykłych typów wariacyjnych. Nie definiuje się dla nich typu.
Rekordy i zmienne
[edytuj]O rekordach można myśleć jak o krotkach ze zdefiniowanymi nazwami pól. Żeby z nich skorzystać najpierw musimy zdefiniować typ rekordowy, następnie gdziekolwiek w programie użyjemy nazw pasujących do tej definicji zostanie użyty ten typ:
# type record = { name : string; age : int };;
type record = { name : string; age : int; }
# let osoba = { name="Tomasz"; age=20 };;
val osoba : record = {name = "Tomasz"; age = 20}
# osoba.name;;
- : string = "Tomasz"
# osoba.name <- "Jurek";;
The record field label name is not mutable
Jak widać żeby pola rekordów były modyfikowalne musimy użyć odpowiedniego słowa kluczowego:
# type record2 = { mutable name : string };;
type record2 = { mutable name : string }
# let osoba = { name="Tomasz" };;
val osoba : record2 = {name = "Tomasz" }
# osoba.name <- "Jurek";;
- : unit = ()
# osoba;;
- : record2 = {name = "Jurek" }
Za pewne ograniczenie może zostać uznany fakt, że nie można używać w jednym programie (w jednej przestrzeni nazw) dwóch rekordów o tych samych nazwach pól, ale z różnymi typami. Ostatnio zdefiniowany rekord zasłoni wszystkie poprzednie definicje.
Skoro doszliśmy tak daleko to pewnie warto dodać słów kilka o zmiennych. Caml zmiennych jako tako wbudowanych nie posiada -- implementuje zmienne jako rekordy z jednym modyfikowalnym polem "content":
# let variable = ref 0;;
val variable : int ref = {contents = 0}
# variable := 5;;
- : unit = ()
# !variable;;
- : int = 5
Tak zdefiniowana została zmienna o nazwie "variable". Robi się to za pomocą słowa "ref" po którym następuje początkowa wartość zmiennej, której nie można pominąć. Ażeby przypisać takiej zmiennej nową wartość używa się operatora ":=", aby zaś odczytać wartość zmiennej wstawiamy przed jej nazwę wykrzyknik. Proste!
Skoro już znamy zmienne możemy pokusić się o przykład na pętlę while:
# let i = ref 0;;
val i : int ref = {contents = 0}
# while (!i < 5) do
printf "Looping, i=%d\n" !i;
i := !i + 1;
done;;
Looping, i=1
Looping, i=2
Looping, i=3
Looping, i=4
- : unit = ()
Pierwszy program
[edytuj]Tym razem program będzie na tyle spory, że proponuję umieścić go w pliku main.ml (lub o innej nazwie), który będziemy kompilować komendą ocamlc -o main main.ml i uruchamiać już klasycznie: ./main
Do większych projektów zbudowanych z paru modułów wykorzystuje się zautomatyzowane mechanizmy budowania. OCaml posiada nakładki na GNU Make, które bardzo ułatwiają z nim pracę.
(* Bez tych linijek musielibyśmy pisać *
* Printf.printf i Scanf.scanf *)
open Printf
open Scanf
(* Typ opisujący rozwiązanie równania;
* Zwracany przez funkcję *)
type solution =
| Zero (* Brak rozwiązań*)
| One of float (* Jedno *)
| Two of (float * float)(* Dwa *)
(* Funkcja znajdująca pierwiastki
* równania kwadratowego *)
let solve a b c =
let delta =
b *. b
-. 4.0 *. a *. c in
if (delta < 0.0) then
Zero
else
let d = sqrt(delta) in
let denom = 2.0 *. a in
if (d = 0.0) then
One (-.b /. denom)
else Two ((-.b +. d) /. denom,
(-.b -. d) /. denom)
let get_data =
(* Funkcja wczytująca dla scanf *)
let read a b c = (a, b, c) in
printf "Proszę podać współczynniki równania:\n";
flush stdout;
scanf "%f %f %f" read (* Zwraca krotkę *)
(* Wczytaj dane *)
let a,b,c = get_data;;
match (solve a b c) with
| Zero -> printf "Brak rozwiązań\n";
| One x -> printf "Jedno rozwiązanie: %.2f" x
| Two (x0, x1) ->
printf "Dwa rozwiązania: %.2f i %.2f\n" x0 x1
Jak już wspomniałem bardzo wiele funkcji buduje się poprzez wielokrotne wywoływanie let ... in, a następnie kwitowanie tego zwróceniem tak utworzonego wyrażenia (czyli napisaniem jego nazwy na końcu). Takie budowane funkcji również pomaga kompilatorowi w znajdywaniu i optymalizowaniu wspólnych wyrażeń.
W podobny sposób zbudowana jest funkcja solve, na początku obliczamy jakieś wstępne dane i nadajemy im etykiety. W późniejszych obliczeniach wykorzystujemy poprzednie etykiety by ostatecznie zwrócić typ zawierający wynik naszych obliczeń.
Wykorzystanie funkcji printf nie powinno sprawić problemów programistom C. Ze względu na brak zmiennych jako takich w języku trochę inaczej wygląda wywołanie funkcji scanf. Oczekuje ona jako argumentu funkcji, która pobierze odczytane przez scanf dane i zrobi z nimi to, czego życzy sobie użytkownik. Wygodne do zastosowania są tutaj funkcje anonimowe.
Prócz funkcji printf/scanf w module Pervasives dostępne są funkcje niższego poziomu; część z nich używaliśmy już wcześniej:
- val print_char : char -> unit
- val print_string : string -> unit
- val print_int : int -> unit
- val print_float : float -> unit
- val print_endline : string -> unit
- val print_newline : unit -> unit
Oraz funkcje do wczytywania danych:
- val read_line : unit -> string
- val read_int : unit -> int
- val read_float : unit -> float
Bardzo dobrze sprawują się jeśli chcemy wyświetlić coś, czego nie musimy dodatkowo formatować. Są prawdopodobnie szybsze od funkcji printf, choć moim zdaniem mają nieporęcznie długie nazwy.
Wyjątki
[edytuj]Caml swoją implementację wyjątków oparł o mechanizm dopasowywania. W module Pervasives znajdziemy definicję funkcji failwith i invalid_arg, które tworzą i "podnoszą" (ang. raise) wyjątki Failure i Invalid_argument z podanym ciągiem znaków. Ponadto znajdziemy tam zdefiniowany wyjątek "Exit", który może być użyty do opuszczania pętli i rekurencji. Prosto możemy tworzyć własne wyjątki.
(* Stwórz wyjątek *)
# exception Empty;;
exception Empty
# let get_head lst =
match lst with
| [] -> raise Empty
| hd :: tl -> hd;;
val head : 'a list -> 'a = <fun>
# get_head [1; 2];;
- : int = 1
# get_head [];;
Exception: Empty.
# let head =
try
get_head []
with
| Empty ->
print_endline "Niestety lista pusta!";
0;; (* Ustaw głowę na zero *)
Niestety lista pusta!
val head : int = 0
Jak widać, wyjątki tworzy się przy wykorzystaniu słowa kluczowego
exception
. Jeśli będziemy chcieli by wyjątek posiadał jakiś
określony typ, stworzymy go następująco:
# exception Error of int;;
exception Error of int
# raise (Error 10);;
Exception: Error 10.
Podczas dopasowywania wartości wyjątków odczytujemy tak jak to ma miejsce przy normalnym dopasowywaniu. Wiele funkcji z biblioteki standardowej używa wyjątków żeby poinformować programistę o niemożności wykonania niektórych zadań.
Nazwane i opcjonalne argumenty funkcji
[edytuj]Jak każdy nowoczesny język Caml udostępnia programiście możliwość nazywania argumentów funkcji oraz tworzenia opcjonalnych argumentów. Również moduły biblioteki o nazwie zakończonej na "Label" udostępniają funkcje z nazwanymi argumentami. Ułatwia to zarządzanie i dokumentowanie kodu.
# let example ~x ~y =
printf "%d %.2f\n" x y;;
val example : x:int -> y:float -> unit = <fun>
# example ~x:4 ~y:10.0;;
4 10.00
- : unit = ()
Ze względu na to, że nie podawanie części argumentów (tzw. częściowa aplikacja) ma w języku specjalne znaczenie, programista musi pokazać kiedy ma zostać użyty opcjonalny argument. Dlatego za opcjonalnymi argumentami musi występować przynajmniej jeden nieopcjonalny i nienazwany. Najczęściej stosuje się typ unit.
# let incr ?(by=1) x = x + by;;
val incr : ?by:int -> int -> int = <fun>
# incr 5;;
- : int = 6
# incr ~by:10 3;;
- : int = 13
(* Są zaimplementowane jako typ wariacyjny.
* Powyższe można w analogiczny sposób zapisać tak: *)
# let incr ?by x =
match by with
| None -> x + 1
| Some step -> x + step;
;;
val incr : ?by:int -> int -> int = <fun>
(* Ich typ to: *)
type 'a option = None | Some of 'a
(* Jeszcze jeden przykład: *)
# let test ?(x = 0) ?(y = 0) () = (x, y);;
val test : ?x:int -> ?y:int -> unit -> int * int = <fun>
# test ();;
- : int * int = (0, 0)
# test ~x:4 ();;
- : int * int = (4, 0)
# test ~x:4 ~y:5 ();;
- : int * int = (4, 5)
# test ~x:4 ~y:5 ;;
- : unit -> int * int = <fun>
Dzięki temu, że funkcja pobiera argument unit
kompilator języka
wie, kiedy chcemy ją ostatecznie zastosować.