OCamlの基礎
最近、OCamlを使った課題を学校で取り組んだので課題を通じて学んだことをまとめてみました。
OCamlとは
OCamlとは、フランスのINRIA(国立情報学自動制御研究所)が開発したプログラミング言語です。(フランスにはなぜか国立の研究所や教育機関が多いです。エンバペやティエリアンリなどのフランスを代表するサッカー選手の多くを輩出しているクレールフォンテーヌ国立研究所(INF)も国立ですし。どのような事情からなのですかね?)
もともとMLというプログラミング言語の方言として存在していたCamlという関数型言語にオブジェクト志向の要素を追加した言語です。ML自体は1970年代に開発された関数型言語で、型推論という特徴を持っています。
出自が関数型言語であるため、一般的なC、C++、Python、Goなどとは異なる構文を持ちます。しかし、オブジェクト指向的要素も備えているため、副作用(関数が引数以外の値を変更したり、入出力操作を行ったりすること)を扱うコードも書くことができます。ただし、OCamlの熟練プログラマーは、関数型プログラミングのパラダイムを重視し、クラスなどのオブジェクト指向機能はあまり使用しない傾向にあります。
関数型言語の代表例としてHaskell(純粋関数型言語)が有名です。Haskellでは単純な出力操作でもモナドという概念を理解する必要があり、初学者には敷居が高いとされています。一方、OCamlはより実用的なアプローチを取っており、print関数などの基本的な操作が直感的に行えます。
そのため、手続き型やオブジェクト指向型の言語しか経験がない人にも、比較的取り組みやすい関数型言語となっています。実際、浅井健一氏による『プログラミングの基礎』という教科書では、OCamlを使用してプログラミングの基礎概念を学ぶことができます。
OCamlの基礎的な構文
Hello, World!
まず、OCamlで基本的な"Hello, World!"プログラムを見てみましょう:
let hello () = Printf.printf "Hello,world!\\n"
let () = hello ()
このコードには、OCamlの重要な概念が含まれています:
-
let hello () =
は関数定義です。()
は引数を取らないことを示す特別な値で、unit型と呼ばれます。unit型は値が1つしかない型で、その値は()
で表されます。 -
Printf.printf
は標準出力への出力関数です。 -
let () = hello ()
は、プログラムのエントリーポイント(C言語でいうmain関数に相当)です。
変数と関数の宣言
OCamlでは、変数と関数の宣言に同じlet
キーワードを使用します:
let hello s = Printf.printf "Hello,%s\\n" s
let () =
let s = "world!" in
hello s
ここで注目すべき点は:
- 変数宣言も関数宣言も
let
を使います。これは、OCamlが「すべてが式」という設計思想を持つためです。 - 関数も変数も「値を名前に束縛する」という同じ操作として扱われます。
- スコープ内での変数宣言では
in
キーワードが必要です。これはその変数の有効範囲を明示します。
関数内での関数定義も可能です:
let hello s =
let print s = Printf.printf "Hello,%s\\n" s in
print s
let () =
let s = "world!" in
hello s
この例では:
-
hello
関数内でprint
関数を定義しています。 -
print
関数はhello
関数のスコープ内でのみ有効です。 - これはレキシカルスコープ(静的スコープ)の例です。
条件式
OCamlの条件式は以下のような構文を持ちます:
let sign x =
if x < 0 then "-"
else if x = 0 then "zero"
else "+"
このsign
関数の特徴:
- C言語と同様の
if-else if-else
構造を持ちますが、then
キーワードが必要です - 明示的な
return
文がありません - 最後に評価された式の値が自動的に戻り値となります
- すべての条件分岐が値を返す必要があります
OCamlでは、if式も含めてすべての式が何らかの値を返します。これは関数型言語の重要な特徴の一つです。
パターンマッチ
パターンマッチは、OCamlの最も強力な機能の一つです。これは他の言語におけるswitch
文を大幅に拡張したものと考えることができます:
let fizzbuzz n =
match (n mod 3, n mod 5) with
| 0, 0 -> "FizzBuzz"
| 0, _ -> "Fizz"
| _, 0 -> "Buzz"
| _, _ -> string_of_int n
このコードの特徴:
-
match ... with
構文を使用します -
|
で各パターンを区切ります -
_
はワイルドカードで、任意の値にマッチします - すべてのパターンを網羅する必要があります(パターンの網羅性チェック)
- 各パターンの後ろに
>
で処理を書きます
パターンマッチは単なる値の比較以上の機能を持ち、データ構造の分解や型の判別なども行えます。
再帰関数
OCamlでは、ループ処理を書く際に再帰関数を使用するのが一般的です:
let fizzbuzz_convert n =
match (n mod 3, n mod 5) with
| 0, 0 -> "FizzBuzz"
| 0, _ -> "Fizz"
| _, 0 -> "Buzz"
| _, _ -> string_of_int n
let rec fizzbuzz start_num end_num =
if start_num > end_num then ()
else (
print_endline (fizzbuzz_convert start_num);
fizzbuzz (start_num + 1) end_num)
let () = fizzbuzz 1 100
このコードの重要な点:
-
rec
キーワードで再帰関数であることを明示します -
()
は unit型の値で、「意味のある値を返さない」ことを表します - 再帰の終了条件(
start_num > end_num
)が明確に定義されています - 各再帰呼び出しで
start_num
が1ずつ増加します
再帰関数を使用する利点:
- 状態の変更を最小限に抑えることができます
- コードの論理が明確になります
- 関数型プログラミングの原則に従います
- コンパイラによる最適化(末尾再帰最適化)が適用される可能性があります
OCamlにはfor
文も存在しますが、関数型プログラミングのスタイルでは再帰を使用するのが一般的です。これは、状態の変更を最小限に抑え、プログラムの理解や検証を容易にするためです。
型システム
OCamlの特徴的な機能の一つに、強力な型システムがあります。
- 静的型付け:コンパイル時に型チェックが行われます
- 型推論:多くの場合、明示的な型注釈が不要です
- 代数的データ型:複雑なデータ構造を表現できます
- パターンマッチング:型安全な方法でデータを分解できます
例えば:
type shape =
| Circle of float (* 半径 *)
| Rectangle of float * float (* 幅と高さ *)
let area = function
| Circle r -> 3.14 *. r *. r
| Rectangle (w, h) -> w *. h
このコードでは、shape
という独自の型を定義し、パターンマッチングを使用して値を処理しています。
Discussion