関数型プログラミングへの関心は年々高まっているが、OCamlは実務での採用例が少なく、日本語の情報も限られている。しかし、OCamlの型システムが提供する安全性の保証は、他の言語にはない独自の強みを持っている。Jane Street、Facebook(Flow、Hack)、Docker(MirageOS)など、実績のある企業が本番環境で採用している言語でもある。
筆者は個人プロジェクトでOCamlを2年ほど使っており、型駆動開発の考え方が他の言語での設計にも好影響を与えている。本記事では、OCamlの型システムを活用した開発手法を、具体的なコードとともに紹介する。
OCamlの最大の武器は代数的データ型(バリアント型)だ。これにより、アプリケーションの状態を網羅的に表現できる。
(* HTTPリクエストの結果を表現 *)
type 'a api_result =
| Success of 'a
| ClientError of { status: int; message: string }
| ServerError of { status: int; retry_after: float option }
| NetworkError of string
| Timeout
(* パターンマッチで全ケースを処理 *)
let handle_result (result : string api_result) =
match result with
| Success body ->
Printf.printf "成功: %s\n" body
| ClientError { status; message } ->
Printf.printf "クライアントエラー %d: %s\n" status message
| ServerError { status; retry_after = Some delay } ->
Printf.printf "サーバーエラー %d, %f秒後にリトライ\n" status delay
| ServerError { status; retry_after = None } ->
Printf.printf "サーバーエラー %d, リトライ不可\n" status
| NetworkError msg ->
Printf.printf "ネットワークエラー: %s\n" msg
| Timeout ->
print_endline "タイムアウト"
ここで重要なのは、コンパイラが全ケースの網羅性を検証するという点だ。新しいバリアントを追加した場合、パターンマッチが不完全なすべての箇所でコンパイルエラーが発生する。実行時にハンドルされないケースが発生するという種類のバグを、完全に防止できる。
OCamlのモジュールシステムは、型安全な抽象化の強力なメカニズムだ。モジュールシグネチャを使って、型の内部表現を隠蔽できる。
(* モジュールシグネチャ:外部に公開するインターフェース *)
module type MONEY = sig
type t (* 型の中身は隠蔽される *)
val of_int : int -> t
val add : t -> t -> t
val sub : t -> t -> t option (* 負の金額を防ぐ *)
val to_string : t -> string
end
(* モジュールの実装 *)
module Money : MONEY = struct
type t = int (* 内部的にはint(セント単位)*)
let of_int cents =
if cents < 0 then invalid_arg "Money: negative amount"
else cents
let add a b = a + b
let sub a b =
if a >= b then Some (a - b)
else None
let to_string cents =
Printf.sprintf "$%d.%02d" (cents / 100) (cents mod 100)
end
この設計により、Money.t型の値はMoneyモジュールが提供する関数でしか操作できない。外部コードが直接整数として扱うことは型エラーとなる。ビジネスロジックの不変条件をコンパイル時に強制できるわけだ。
OCamlのファンクターは、モジュールを引数に取ってモジュールを返す「モジュールレベルの関数」だ。これにより、型安全なジェネリックプログラミングが可能になる。
(* 比較可能な型のシグネチャ *)
module type COMPARABLE = sig
type t
val compare : t -> t -> int
end
(* ソート済みリストを提供するファンクター *)
module SortedList (Elem : COMPARABLE) = struct
type t = Elem.t list
let empty = []
let rec insert x = function
| [] -> [x]
| hd :: tl as l ->
if Elem.compare x hd <= 0 then x :: l
else hd :: insert x tl
let to_list t = t
let merge a b =
List.fold_left (fun acc x -> insert x acc) a b
end
(* 整数のソート済みリスト *)
module IntSorted = SortedList(Int)
(* 文字列のソート済みリスト *)
module StringSorted = SortedList(String)
let () =
let nums = IntSorted.empty
|> IntSorted.insert 3
|> IntSorted.insert 1
|> IntSorted.insert 4
|> IntSorted.insert 1
|> IntSorted.insert 5 in
List.iter (Printf.printf "%d ") (IntSorted.to_list nums)
ファンクターの利点は、型の一貫性をコンパイラが保証することだ。IntSortedに文字列を挿入しようとすればコンパイルエラーになる。Javaのジェネリクスと似た役割だが、モジュールレベルで動作するため、より大きな単位での抽象化が可能だ。
型駆動開発では、実装の前にまず型を設計する。具体的なワークフローは以下の通りだ。
このアプローチの最大の利点は、設計の欠陥をコーディング中に早期発見できることだ。型が合わない場合、それはドメインモデルの理解が不十分であることを意味する。コンパイラが設計レビュアーの役割を果たしてくれる。
OCamlの開発環境はここ数年で大きく改善された。パッケージマネージャのopam、ビルドシステムのdune、LSP対応のエディタプラグインにより、モダンな開発体験が得られる。
# opamの初期化とプロジェクト作成
opam init
opam switch create . ocaml-base-compiler.5.1.0
eval \$(opam env)
# duneプロジェクトの初期化
dune init project my_project
# ビルドと実行
dune build
dune exec my_project
# テストの実行
dune runtest
OCaml 5.x系ではマルチコア対応のEffectハンドラが導入され、並行処理の表現力も大きく向上している。パフォーマンス面でも、GC(ガベージコレクション)の改善により、レイテンシが安定している。
OCamlで学んだ型駆動開発の考え方は、TypeScript、Rust、Kotlinなど他の言語にも応用できる。特にTypeScriptのDiscriminated Unions(判別可能な共用体)はOCamlのバリアント型に近い概念であり、同様の設計パターンを適用できる。
重要なのは「不正な状態を型レベルで表現不可能にする」という設計哲学だ。nullableな値にはOption型を使い、エラーにはResult型を使い、状態の遷移をバリアント型で表現する。この考え方を身につけることで、どの言語を使っていても堅牢なコードが書けるようになる。
OCamlの型システムは、単なる型チェック以上の価値を提供する。ドメインの概念を正確にモデリングし、不正な状態をコンパイル時に排除する——これが型駆動開発の本質だ。OCaml自体を本番環境で使わなくとも、ここで得られる設計思考は確実にエンジニアとしての引き出しを増やしてくれる。興味があれば、まず小さなCLIツールから作ってみることを勧める。