cv.cm

コーディング · プログラミング言語

OCaml入門:初心者向けチュートリアル

約10分で読めます

結論

OCamlは、ML系に属する静的型付けの関数型言語です。コンパイラが型を推論し、プログラムを実行する前に多くの間違いを見つけ、高速なネイティブコードを生成します。プログラムは、イミュータブルな値、バリアント(タグ付き共用体)、パターンマッチ、モジュールで組み立てます。opamでインストールし、ocaml hello.mlでファイルを実行するか、ocamloptでコンパイルします。

OCamlとは何か、誰が使うのか

OCamlはフランスのInriaで生まれ、現在はJane StreetやTaridesなどの企業を含むオープンソースのコミュニティによって開発されています。関数型プログラミングに、オプションの命令型の機能(変更可能な参照、配列、ループ)と、ほとんどのコードでは必要ないオブジェクトシステムを組み合わせています。OCaml 5では、ドメインとエフェクトハンドラーによる共有メモリ並列が加わりました。

OCamlは、コンパイラやプログラム解析ツール、Rocq(旧Coq)のような証明支援系、取引システム(Jane Streetが最もよく知られた産業利用者です)、MetaのFlowやInferのような開発者向けツールに使われています。関数型プログラミングや型システムを教える言語としても定番です。

OCamlのインストールとhello, worldの実行

OCamlの標準的なインストール方法は、OCamlのパッケージマネージャーであるopamです。opamをインストールしたら、opam initを一度実行します。これでコンパイラがセットアップされます。続いて、opam install dune utopで、ビルドツールのduneと、より使いやすいREPLのutopをインストールします。

(* hello.ml *)
let () = print_endline "Hello, world!"
ocaml hello.ml            # run with the interpreter
ocamlopt -o hello hello.ml
./hello                   # native executable
# Hello, world!

let () = ...はエントリーポイントの標準的な書き方です。式を評価し、それがunit(値なし)を返すことを確認します。REPL(utopまたはocaml)では各フレーズの末尾に;;を付けますが、ソースファイルでは;;は必要ありません。実際のプロジェクトでは、dune init project myappでプロジェクトを作り、dune buildでコンパイルします。

値と基本の型

型例補足
int4264ビットシステムでは63ビット。演算子は+ - * / mod
float3.14専用の演算子を使います:+. -. *. /.
string"hello"変更できないバイト列。結合は^
char'a'1バイト
booltrue, false&&, ||, not
unit()結果を持たない式の値
let name = "Ana"
let age = 30
let height = 1.68 *. 100.0
let () = Printf.printf "%s is %d, %.0f cm\n" name age height
(* Ana is 30, 168 cm *)
let count = ref 0
let () = incr count; Printf.printf "count = %d\n" !count

束縛はイミュータブルです。refは変更可能なセルで、!rで読み、r := vで書きます。OCamlはintとfloatを自動的に変換しません。float_of_intとint_of_floatを使ってください。

制御構文と関数

ifは式です。関数はletで定義し、再帰関数にはlet recが必要です。引数は空白で区切り、どの関数もカリー化されているので、部分適用は自然にできます。

let sign n =
  if n > 0 then "positive"
  else if n < 0 then "negative"
  else "zero"
let rec factorial n = if n <= 1 then 1 else n * factorial (n - 1)
let add x y = x + y          (* int -> int -> int *)
let add5 = add 5
let () =
  for i = 1 to 3 do
    Printf.printf "%d squared is %d\n" i (i * i)
  done;
  Printf.printf "%s %d %d\n" (sign (-2)) (factorial 5) (add5 10)
(* negative 120 15 *)

sign (-2)のかっこに注意してください。かっこがないと、OCamlはsign - 2を引き算として読みます。unitを返す式を順に実行するには;を使います。

リスト・配列・タプル・レコード

構造リテラル補足
List[1; 2; 3]イミュータブルな単方向連結リスト。x :: xsで先頭に追加します
Array[| 1; 2; 3 |]固定サイズで変更可能。a.(0)で添字アクセスします
Tuple("Ana", 30)異なる型の値を固定個数まとめたもの
Record{ name = "Ana"; age = 30 }名前付きフィールド。先に型を宣言します
Hashtbl / MapHashtbl.create 16変更可能なハッシュテーブル / イミュータブルな平衡木のマップ
type person = { name : string; age : int }
let people = [ { name = "Ana"; age = 30 }; { name = "Ben"; age = 17 } ]
let () =
  people
  |> List.filter (fun p -> p.age >= 18)
  |> List.map (fun p -> p.name)
  |> List.iter print_endline
(* Ana *)
let older = { (List.hd people) with age = 31 }

パイプライン演算子x |> fはf xを意味します。Listモジュールにはmap、filter、fold_left、find_optなどが揃っています。

特徴的な機能:バリアント・パターンマッチ・モジュール

バリアント型は、値がとりうるすべてのケースを列挙します。パターンマッチはケースごとに値を分解し、ケースが足りないとコンパイラが警告するので、安全にリファクタリングできます。

type shape =
  | Circle of float
  | Rectangle of float * float
let area = function
  | Circle r -> Float.pi *. r *. r
  | Rectangle (w, h) -> w *. h
let () =
  List.iter (fun s -> Printf.printf "%.2f\n" (area s))
    [ Circle 1.0; Rectangle (2.0, 3.0) ]
(* 3.14 then 6.00 *)
module StringMap = Map.Make (String)
let ages = StringMap.(empty |> add "Ana" 30 |> add "Ben" 17)
let () = Printf.printf "%d\n" (StringMap.find "Ana" ages)   (* 30 *)

モジュールは型と関数をまとめます。すべてのファイルがモジュールです(list.mlはListになります)。ファンクターは、モジュールからモジュールへの関数です。Map.Make (String)は、キーが文字列のマップモジュールを作ります。シグネチャ(.mliファイル)は、実装の詳細を隠します。

エラー処理

OCamlには3つの手段があります。値がないかもしれない場合のoption、情報付きで失敗しうる処理のresult、例外的な状況のための例外です。

let parse_age s =
  match int_of_string_opt s with
  | Some n when n >= 0 -> Ok n
  | Some _ -> Error "Age must not be negative"
  | None -> Error "Not a number"
let () =
  match parse_age "abc" with
  | Ok n -> Printf.printf "Age %d\n" n
  | Error msg -> print_endline ("Error: " ^ msg)
(* Error: Not a number *)
exception Empty_input
let first = function [] -> raise Empty_input | x :: _ -> x
let () =
  try print_int (first []) with Empty_input -> print_endline "empty list"

小さな完成プログラム:単語の出現頻度

let () =
  let text = "the cat and the hat and the bat" in
  let counts = Hashtbl.create 16 in
  String.split_on_char ' ' text
  |> List.iter (fun w ->
         let n = Option.value (Hashtbl.find_opt counts w) ~default:0 in
         Hashtbl.replace counts w (n + 1));
  Hashtbl.to_seq counts
  |> List.of_seq
  |> List.sort (fun (_, a) (_, b) -> compare b a)
  |> List.filteri (fun i _ -> i < 2)
  |> List.iter (fun (w, n) -> Printf.printf "%s %d\n" w n)
(* the 3
   and 2 *)

このプログラムは、変更可能なハッシュテーブルで単語を数え、テーブルを(word, count)のペアのリストに変換し、回数の多い順に並べ替えて最初の2つを表示します。~default:0はラベル付き引数です。

次に学ぶこと

  • ocaml.org:インストールガイド、「A Tour of OCaml」、Docsにあるその他のチュートリアル。
  • 言語と標準ライブラリの完全なリファレンスであるOCamlマニュアル。
  • プロジェクトのビルドのためのduneと、ライブラリのインストールのためのopamを学ぶ。
  • より大きな実用的な例を載せた無料のオンライン書籍『Real World OCaml』に挑戦する。

よくある質問

OCamlは習得が難しいですか?

構文は小さいのですが、命令型言語から来た場合、型、再帰、パターンマッチで考えるには練習が必要です。型チェッカーのエラーメッセージは、すぐに読みやすくなります。

OCamlはなぜ浮動小数点数用に別の演算子があるのですか?

OCamlは算術演算子をオーバーロードしないので、+はintにだけ、+.はfloatにだけ使えます。これにより、型推論がシンプルで予測しやすくなっています。

OCamlとF#の違いは何ですか?

F#はOCamlから派生し、中核の構文の多くを共有していますが、F#は.NET上で動き、インデントに意味があります。一方OCamlは、ネイティブコードまたはバイトコードにコンパイルされ、ファンクターを持つより強力なモジュールシステムがあります。

OCamlで;;が必要なのはいつですか?

対話型のトップレベル(ocamlやutop)だけです。入力した内容を評価するようREPLに伝えます。.mlのソースファイルでは、通常は省略します。

コーディング講座の一覧