OCaml モジュールとシグネチャ

OCaml モジュールとシグネチャ

ソースファイル .mlコンパイルするとオブジェクトファイル .cmo.cmx .o と同時にインターフェイスファイル .cmi が生成される。

この .cmi について調べてみた。理解してしまえば何ということはないのだけれど、少し時間がかかった。

  • .mliコンパイルすると .cmi が生成される。
  • .mli がない場合は .ml から .cmi が生成される。

では .mli とは何だろう。

ではモジュールとは、シグネチャとは何だろう。

モジュールとは

詳細な解説は以下の記事、資料を読むとよさそう。

ただ、一度に全部を理解しようとするのはかなり困難だったので、ひとまずは「関連する処理やデータなどのまとまり」くらいの理解でよいと思う。Python や Go でいうパッケージと同じようなものだと考えればよいだろう。

OCaml では一つのファイルを一つのモジュールとするそう。前回書いた、階乗を求める処理でもモジュールとして扱っていた。

fact.mlFact モジュールとなる。

(* fact.ml *)

let rec fact n =
  if n = 0 then
    1
  else
    n * fact (n - 1)

利用する側からは open で読み込む。

(* main.ml *)

open Fact
let () = print_int (fact 10); print_newline()

もしくは、モジュール名.関数 の形でドット . をつけてモジュール内の関数を利用する。

(* main.ml *)

let () = print_int (Fact.fact 10); print_newline()

モジュールに別名をつけて読み込む

Pythonas で別名をつけて import するのと同じように

import numpy as np

以下の書き方で別名をつけて open することができる。

module F = Fact
let () = print_int (F.fact 10); print_newline()
  • この場合 open というキーワードは使わない。
  • 別名 F の部分は大文字で始める必要あり。

シグネチャとは

モジュールについては他の言語でも似たような仕組みはあるので、何となくではあるけれどわかった。次はシグネチャである。

OCaml におけるシグネチャは、モジュールの中身に対するアクセスの可否を制御する仕組みで、個々の関数には private とか public とかつけずに別途まとめて管理できるようになっている。

今まで触ったことのある言語(Python, Go, JavaScript あたり)ではこれに該当するものはなかったと思われ、新鮮に感じた。言語によっては、関数の仮引数や戻り値の型、アクセス修飾子などのことを指してシグネチャと呼ぶ場合があるみたいだけれど。

Python だと名前の頭にアンダースコア ___ をつけることで private 扱いになり、Go だと名前の頭文字が大文字なら外部からアクセス可能で小文字だとアクセス不可、という仕様になっている。

で、OCaml の場合。

Fact モジュールを少し拡張して試してみる。階乗 n! のバリエーションとして多重階乗というものがあるらしい。二重階乗 n!! であれば一つ飛ばしで積をとる。

5!! = 5 * 3 * 1
10!! = 10 * 8 * 6 * 4 * 2

この多重階乗を求める関数 multifact を書いてみた。

(* fact.ml *)

let rec fact n =
  if n = 0 then
    1
  else
    n * fact (n - 1)

let rec multifact x n =
  match n with
  | 0 -> 1
  | 1 -> 1
  | _ -> if x > n then
      n
    else
      n * multifact x (n - x)

let doublefact = multifact 2

モジュールを利用する側のコード。

(* main.ml *)

open Fact
let () =
  print_int (fact 10); print_newline ();
  print_int (multifact 2 10); print_newline ();
  print_int (doublefact 10); print_newline ();
;;

実行結果はこうなる。

# コンパイルして
$ ocamlc -o fact10 fact.ml main.ml

# 実行する
$ ./fact10
3628800 # fact 10
3840    # multifact 2 10
3840    # doublefact 10

シグネチャ*.mli に書く

次に Fact モジュールのシグネチャfact.mli に書く。公開するものだけを書き、シグネチャに書かれていないものは外部からアクセスできなくなる。

試しに multifact は非公開としてみる。

(* fact.mli *)

val fact : int -> int
val doublefact : int -> int

このシグネチャの書式はコンパイラ-i オプションをつけて当該ファイルを渡すと確認できる。-i オプションのみの場合は実際のコンパイルはしない(.cmo ファイルは生成されない)。

$ ocamlc -i fact.ml
val fact : int -> int
val multifact : int -> int -> int
val doublefact : int -> int

*.mli にリダイレクトして、公開したくないものを削除すればよい。

$ ocamlc -i fact.ml > fact.mli

ファイル *.mli が存在する場合は、*.ml よりも先にコンパイルしないとエラーになる。

$ ocamlc -c fact.ml
File "fact.ml", line 1:
Error: Could not find the .cmi file for interface fact.mli.

fact.mliコンパイルして、

$ ocamlc -c fact.mli # fact.cmi が生成される

fact.mlコンパイルして、

$ ocamlc -c fact.ml # fact.cmo が生成される

main.mlコンパイルしようとすると、エラー!

$ ocamlc -c main.ml
File "main.ml", line 4, characters 13-22:
Error: Unbound value multifact

ふむふむ。

それでは修正してみよう。main.ml から multifact を呼ぼうとしている箇所をコメントアウトして、

(* main.ml *)

open Fact
let () =
  print_int (fact 10); print_newline ();
  (* print_int (multifact 2 10); print_newline (); *)
  print_int (doublefact 10); print_newline ();
;;

コンパイルすると、今度は無事通った。

$ ocamlc -c main.ml # main.cmo が生成される
$ ls
. .. fact.cmi fact.cmo fact.ml fact.mli main.cmi main.cmo main.ml

これで必要なファイルが揃ったので、以下のように -o オプションで実行ファイルを出力する。

$ ocamlc -o fact10 fact.cmo main.cmo
$ ./fact10
3628800
3840

あるいは、以下のようにして最初から実行ファイルを出力することも可能。この場合もコンパイラには *.mli を先に渡す必要あり。

$ ocamlc -o fact10 fact.mli fact.ml main.ml

シグネチャを書かない場合は全て公開される

先のエントリで見たようにシグネチャを書かなくても *.ml から *.cmi が生成されていた。この場合は、モジュールの中身は全て公開されることになる。

まぁ OCaml プログラマが意図的にシグネチャを書かないというケースはなさそう(想像)だから、気にしなくていいんじゃないかな。

参照