Tomasz bla Fortuna
Abstract
Aktualna wersja tekstu znajduje się na wikibooks:
http://pl.wikibooks.org/wiki/OCaml
Ta tutaj nie jest już aktualizowana, ale zostawię ją ze względów "historycznych".
Tekst, który widzisz wciąż jest nieskończony i wymaga na pewno poprawek. Nie
miałem zamiaru stworzenia pełnego opisu języka, raczej miał to być
wstęp, zarys, który miał ułatwić rozpoczęcie zabawy (i pracy!) z językiem
OCaml. Przedstawić jego cechy, mechanizmy i tym samym zaciekawić.
Niemniej jednak wyszedł dość obszerny. Zapraszam do czytania. W razie
znalezionych jakichkolwiek błędów proszę pisać albo na jabbera, albo na gadu,
albo na maila, albo w ostateczności zostawić w komentarzach. Również
zapraszam do rozmowy w razie jakiś pytań, choć nie uważam się w żadnym
wypadku za jakikolwiek autorytet.
Spis treści
Historia pewnej znajomości
Jakiś czas temu (końcówka 2006 roku) poczułem potrzebę nauczenia się jakiegoś
języka funkcyjnego. Pomimo paru lat programowania w językach takich jak
C/C++, PHP, Ruby, języki funkcyjne były dla mnie wciąż absolutnie obce -- nie
rozumiałem co mają takiego, czego nie ma w języku C (W C przecież są funkcje,
nie?). No ale już wiem.
Zacząłem od przeglądu istniejących języków tego typu. Na pierwszy ogień padł
Haskell i Lisp, ale po rozmowach na FreeNode, paru próbach napisania w nich
prostych programów, przejrzeniu tutoriali -- oba odpadły. Trudno mi na dziś
dzień przypomnieć sobie dlaczego, ale nie sprawiły wrażenia języków, w
których chciałbym cokolwiek napisać.
Objective Caml (w skrócie OCaml lub po prostu Caml) natomiast miał parę
własności, które były interesujące:
-
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 ma możliwość kompilowania do zarówno przenośnego kodu
bajtowego (bytecodu) tak jak np. Java, lub do szybkich natywnych
binarek (jak C).
-
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.
-
Zawiera bardzo efektywny garbage collector.
-
Ponadto 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 absolutnie
obca.
Strona główna projektu znajduje się pod adresem:
http://caml.inria.fr/. Można znaleźć tam
trochę dokładniejszej dokumentacji, opis bibliotek języka oraz paczki do pobrania.
Środowisko pracy
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.
Po miłej i szybkiej instalacji (której tu opisywać nie ma sensu) 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 w Ocamlu.
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 rozwiązanie, którym jest: wrapper biblioteki
readline. Jeden z nich nazywał się "rlwrap" i nie działał mi dobrze. Drugi, o
nazwie "ledit" zadziałał wprost idealnie. Od razu do .bashrc wrzuciłem
linijkę:
alias ocaml="ledit ocaml"
I na tym skończyły się moje niewygody z "ocaml". W razie czego zalecam
spróbowanie obu wrapperów.
Krótki opis najważniejszych cech języka
Caml jak już wspomniałem 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. Pomimo, że 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), trochę przyspiesza tworzenie programów
(zwalniając programistę z obowiązku deklarowania zmiennych), przyspiesza
wykonywanie programu przez dokładniejsze przeprowadzenie optymalizacji to
wprowadza 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 ciąg znaków
na liczbę), 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 najchętniej
przekazałbym poprzez przykłady, mi przynajmniej najszybciej się zawsze z nich
uczy.
"Step by step you're gonna make yourself code
better"
Jeśli mamy przygotowane środowisko programistyczne do nauki, możemy przerobić
parę prostych programów. Uruchamiamy "ocaml" i wpisujemy pierwszy banalny
kod:
$ ocaml
Objective Caml version 3.09.3
# 5 + 5;;
- : int = 10
#
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 "kod" zwrócił taką informację: "- : int = 10",
która zawiera wynikowy (ang. inferred) typ wyrażenia (int) oraz jego wartość (10).
Myślnik 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 =
# add 5 6;;
- : int = 11
# add 5 (a * 5 + 5);;
- : int = 60
# let addfive = add 5;;
val addfive : int -> int =
# addfive 40;;
- : int = 45
# let add2 a b =
let sum = a + b in
sum
;;
val add2 : int -> int -> int =
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ą swoistą funkcję samą w sobie. Funkcję,
która nie pobiera żadnego argumentu i zwraca typ int. 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
Ładnie za to prezentują to, że w istocie mamy tutaj do czynienia z funkcją.
Wracając do naszego poprzedniego sporego przykładu; w linii 10 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 = ", 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
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 =
(* 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 =
# add_three 1 2;;
- : int -> int =
# 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 =
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 =
# let sin' = deriv sin 1e-6;;
val sin' : float -> float =
# 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 =
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 =
# let sqrt_cos = compose sqrt cos;;
val sqrt_cos : float -> float =
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, iteracje
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 =
# 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 =
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 =
(* 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
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 =
# 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 (ang. tuples) i tablice
Krotki 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 =
(* 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)]
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
w 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
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 =
# 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żywany 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 =
# 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"
;;
(Zielony wcale nie jest moim ulubionym kolorem.)
Możemy użyć dopasowywania do jeszcze innego zdefiniowania funkcji
obliczającej ciąg fibonacciego. Tym razem definicja małpuje bezpośrednio
definicje 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 tl num
;;
val add_to_list : int list -> int -> int list =
# 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 =
# 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
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 = ()
Większy przykład, I/O i programy "stand-alone"
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
(* 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.
Wstęp do wyjątków
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 =
# 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
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 =
# 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 =
# 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 =
(* 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 =
# 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 =
Dzięki temu, że funkcja pobiera argument
unit kompilator języka
wie, kiedy chcemy ją ostatecznie zastosować.
Przydatne linki
Bardzo dobry tekst, który pozwala zgłębić co nieco tajniki OCamla:
OCaml Tutorial. Klasyczny wstęp,
również bardzo przydatny:
User Manual.
Opis bibliotek języka OCaml:
Language Reference
Zapraszamy do kodowania. ;-)
Licencja
Copyright (C) Tomasz bla Fortuna. Permission is granted to copy, distribute
and/or modify this document under the terms of the GNU Free Documentation
License Version 1.2 published by the Free Software Foundation A copy of the
license is available from address http://www.gnu.org/licenses/fdl.html.
Add a comment [+] Hide the comment form [-]