W językach, których implementacje wyposażone są w tryb interaktywny (REPL), zazwyczaj podstawowe funkcjonalności danej technologii poznaje się właśnie w tym środowisku. Jest to po prostu najszybszy i najłatwiejszy sposób kontaktu z nowym językiem. Nie inaczej jest z OCamlem – odpalamy utop i już jesteśmy gotowi zgłębiać tajniki programowania funkcyjnego. Jednak w miarę jak pisane skrypty stają się coraz bardziej rozbudowane, przychodzi potrzeba porzucenia trybu interaktywnego na rzecz programów zapisywanych w plikach. Wrzucamy zatem jakiś przykładowy kod do pliku hello.ml
i… rozpoczynamy poszukiwanie odpowiedzi na pytanie którego narzędzia użyć do skompilowania naszego programu. Wybór jest bowiem naprawdę spory: ocamlc, ocamlopt. ocamlc.opt, ocamlopt.opt, ocamlfind, ocamlbuild, ocamlrun. Spory jest więc również dylemat, przed którym można w tej sytuacji stanąć.
Spis treści
Bajtowo lub natywnie
Podstawowe rozróżnienie, które można zastosować wobec dostępnych narzędzi to docelowy format plików wyjściowych – źródła napisanych w OCamlu programów można kompilować albo do kodu bajtowego (który musi być później interpretowany przez kolejne narzędzie), albo do kodu natywnego (czyli postaci, która może zostać uruchomiona bezpośrednio przez system operacyjny):
- do kodu bajtowego kompiluje ocamlc i ocamlc.opt
- do kodu natywnego kompiluje ocamlopt i ocamlopt.opt
Program skompilowany do postaci natywnej wykonuje się zazwyczaj szybciej, jeśli więc zależy nam na wydajności, powinniśmy wybrać właśnie tę opcję. Na tej zasadzie opiera się też różnica między narzędziami ocamlc i ocamlc.opt – funkcjonalnie są to te same kompilatory, ale ocamlc jest kompilatorem skompilowanym do kodu bajtowego, a ocamlc.opt kompilatorem z wersji natywnej (więc będzie kompilował szybciej). Sufiks opt oznacza tu oczywiście wersję zoptymalizowaną (ang. optimized). Analogicznie sytuacja ma się z kompilującym do kodu natywnego programem ocamlopt oraz ocamlopt.opt. Jeśli chcecie samodzielnie zweryfikować prawdziwość moich słów możecie wpisać w konsoli:
1 2 3 4 5 6 7 8 9 10 |
$ ocamlrun ocamlc -v The OCaml compiler, version 4.02.3 Standard library directory: /usr/lib/ocaml $ ocamlrun ocamlc.opt -v Fatal error: the file '/usr/bin/ocamlc.opt' is not a bytecode executable file $ ocamlrun ocamlopt -v The OCaml native-code compiler, version 4.02.3 Standard library directory: /usr/lib/ocaml $ ocamlrun ocamlopt.opt -v Fatal error: the file '/usr/bin/ocamlopt.opt' is not a bytecode executable file |
ocamlrun to interpreter kodu bajtowego OCamla i, jak widać, potrafi on uruchomić kompilatory ocamlc oraz ocamlopt, ale z ich wersjami natywnymi ma już problem.
Nasza pierwsza kompilacja
Przejdźmy więc do praktyki i sprawdźmy czy pokrywa się ona z piękną teorią. Dla ułatwienia niechaj treścią pliku z kodem źródłowym będzie najprostsze co być może:
1 |
let () = print_endline "Hello, OCaml" |
Oto jak możemy teraz postąpić z zapisanym programem:
1 2 3 4 5 |
$ ocamlc hello.ml # Kompilacja do kodu bajtowego # lub: $ ocamlopt hello.ml # Kompilacja do kodu natywnego # lub: $ ocaml hello.ml # Kompilacja i uruchomienie w top-levelu |
Rezultatem dwóch pierwszych poleceń będzie utworzenie pliku wykonywalnego a.out, zaś ostatnia komenda skompiluje i uruchomi program, nie pozostawiając po sobie żadnych śladów. Proste? Proste. Jak się jednak okazuje, kompilatory OCamla generują również kilka innych plików – nie są one wprawdzie potrzebne do działania skompilowanej aplikacji, ale warto wiedzieć czym w ogóle są.
Plik | Opis |
---|---|
.ml | Kod źródłowy |
.mli | Specyfikacja interfejsu |
.cmi | Skompilowany (z pliku .mli lub .ml jeśli brak tego pierwszego) interfejs |
.cmo | Plik obiektowy (bajtowy) |
.o | Plik obiektowy (natywny) |
.cmx | Zawiera dodatkowe informacje dot. natywnego pliku obiektowego |
.cma | Biblioteka kodu bajtowego (zbiór plików .cmo) |
.a | Biblioteka natywna (zbiór plików .o) |
.cmxa | Informacje związane z biblioteką natywną (zbiór plików .cmx) |
Informacje te mogą przydać się zwłaszcza wtedy, kiedy planujemy korzystać z dodatkowych bibliotek, bądź sami chcemy w tej formie dystrybuować nasz kod.
I pierwsze problemy
Wszystko idzie gładko do momentu, w którym nie postanowimy skorzystać z zewnętrznych modułów, takich jak chociażby popularny Core (tworzony przez Jane Street). W ramach przykładu skomplikujmy odrobinę nasz kod, aby korzystał on z funkcji pochodzącej z modułu Core.Std
:
1 2 3 4 5 6 7 |
open Core.Std let () = let languages = ["ML"; "OCaml"] in let our_language = List.last_exn languages in let text = "Hello, " ^ our_language in print_endline text |
Kiedy spróbujemy ów kod skompilować, pojawi się prosty do przewidzenia błąd:
1 2 3 |
$ ocamlc hello.ml File "hello.ml", line 1, characters 5-13: Error: Unbound module Core |
Oczywiście – trzeba wszak poinformować kompilator o lokalizacji dodatkowej biblioteki. Dla ułatwienia dokonamy stworzenia pliku wykonywalnego w dwóch etapach – najpierw tylko skompilujemy kod źródłowy (odpowiada za to flaga -c
), a dopiero później, za pomocą osobnej komendy, zlinkujemy go. Metodą prób i błędów (gdyż okazuje się, że zaincludowanie samego tylko Core
nie wystarczy) dochodzimy do skonstruowania następującego polecenia:
1 |
ocamlc -c -I ~/.opam/system/lib/core_kernel/ -I ~/.opam/system/lib/core/ hello.ml |
Hurra, udało się! Kompilator wygenerował plik obiektowy (hello.cmo) oraz skompilowany interfejs (hello.cmi). Teraz czas na fazę ostateczną, czyli linkowanie. Tutaj niestety sprawa znacznie się komplikuje, bo liczba bibliotek, które musimy wskazać robi się naprawdę spora. Moduł Core
wymaga Core_kernel
, z kolei Core_kernel
korzysta z Variantslib
i tak dalej, i tak dalej. W rezultacie aby dokończyć dzieła musimy wpisać:
1 |
ocamlc -thread /usr/lib/ocaml/unix.cma /usr/lib/ocaml/threads/threads.cma /usr/lib/ocaml/bigarray.cma ~/.opam/system/lib/bin_prot/bin_prot.cma ~/.opam/system/lib/fieldslib/fieldslib.cma /usr/lib/ocaml/nums.cma ~/.opam/system/lib/sexplib/sexplib.cma ~/.opam/system/lib/ppx_assert/ppx_assert_lib.cma ~/.opam/system/lib/ppx_bench/ppx_bench_lib.cma ~/.opam/system/lib/ppx_expect/expect_test_common.cma ~/.opam/system/lib/ppx_expect/expect_test_config.cma ~/.opam/system/lib/ppx_inline_test/inline_test_config.cma ~/.opam/system/lib/ppx_inline_test/ppx_inline_test_lib.cma ~/.opam/system/lib/ppx_expect/expect_test_collector.cma ~/.opam/system/lib/result/result.cma ~/.opam/system/lib/typerep/typerep_lib.cma ~/.opam/system/lib/variantslib/variantslib.cma ~/.opam/system/lib/core_kernel/core_kernel.cma ~/.opam/system/lib/sexplib/sexplib_unix.cma ~/.opam/system/lib/core/core.cma hello.cmo |
Wychodzi na to, że polecenie kompilacji jest dłuższe niż sam kod źródłowy, co zakrawa już na pewien absurd. Na szczęście nie tędy droga i aby skompilować OCamlowy program nie musimy wcale poświęcać czasu na ręczne znajdowanie zależności.
Niezastąpiony ocamlfind
Narzędziem, które wybawia nas z tarapatów jest ocamlfind. Chcąc skompilować program do kodu bajtowego wystarczy że wpiszemy:
1 |
ocamlfind ocamlc -package core -thread -linkpkg hello.ml |
Bibliotekę thread
musimy zalinkować, ponieważ wymaga tego Core
, ale to wszystko, nie musimy przejmować się żadnymi innymi pośrednimi zależnościami. Chcąc zamiast ocamlc użyć ocamlopt wystarczy natomiast że wywołamy polecenie:
1 |
ocamlfind ocamlopt -package core -thread -linkpkg hello.ml |
A jak wywołać kompilację przy pomocy np. ocamlopt.opt? Wystarczy zedytować plik ~/.opam/system/lib/findlib.conf (choć prawdopodobnie już domyślnie ustawione jest, że wywołanie ocamlfind ocamlopt...
użyje narzędzia ocamlopt.opt).
Do większych projektów – ocamlbuild
Opisane w poprzednich akapitach metody kompilacji są dobre, ale ich stosowanie w przypadku projektu składającego się z wielu plików z kodem źródłowym byłoby nieporozumieniem. Pracując nad tego typu projektem zazwyczaj nie chcemy kompilować za każdym razem całego kodu, a jedynie te fragmenty, które uległy zmianie od ostatniej kompilacji. Dobrze byłoby też mieć możliwość budowania różnych targetów (np. i szybkiego puszczania testów jednostkowych), a do tego wszystkiego na pewno wygodniej byłoby posługiwać się jakimś mniej skomplikowanym poleceniem. Doskonałym narzędziem, które ułatwia budowanie projektów jest oczywiście Makefile.
A jak to się ma do OCamla? Otóż jednym z programów, które łatwo można zaprząc do współpracy z Makefile’ami jest ocamlbuild. Odpowiednikami wcześniej używanych komend ocamlfind ocamlopt
oraz ocamlfind ocamlc
są:
1 2 |
ocamlbuild -use-ocamlfind -package core_extended -tag thread hello.native ocamlbuild -use-ocamlfind -package core_extended -tag thread hello.byte |
Funkcjonalności ocamlbuilda jest naprawdę sporo i znajdując się na etapie, na którym zachodzi konieczność skorzystania z niego, najlepiej jest po prostu sięgnąć do dokumentacji [5], która jasno tłumaczy możliwe opcje i obrazuje je licznymi przykładami.