シグネチャ

前のエントリの例のようにシグネチャをコンパイラに推論させるのではなく,自分で書くこともできる。そのとき,モジュールの外部には公開したくない関数や,定義した型の詳細を隠蔽することもできる。一般には:

  • シグネチャを定義する
  • そのシグネチャをモジュールに与える

という手順を踏む。

たとえば次のようにモジュールを定義したとする。

# module Table =
struct
type ('a, 'b) t = Empty | Entry of 'a * 'b * ('a, 'b) t
let empty = Empty
let add key datum table = Entry (key, datum, table)
let rec retrieve key = function
Empty -> None
| Entry (key', datum, rest) ->
if key = key' then Some datum else retrieve key rest
let rec delete key = function
Empty -> Empty
| Entry (key', datum, rest) ->
if key = key' then delete key rest
else Entry (key', datum, delete key rest)
let rec dump = function
Empty -> []
| Entry (key, datum, rest) ->
(key, datum) :: (dump (delete key rest))
end;;
module Table :
sig
type ('a, 'b) t = Empty | Entry of 'a * 'b * ('a, 'b) t
val empty : ('a, 'b) t
val add : 'a -> 'b -> ('a, 'b) t -> ('a, 'b) t
val retrieve : 'a -> ('a, 'b) t -> 'b option
val delete : 'a -> ('a, 'b) t -> ('a, 'b) t
val dump : ('a, 'b) t -> ('a * 'b) list
end

見てわかるとおり,データ型ひとつと関数を4つ定義している。

さて,ここで Table.t型の詳細とdelete関数を隠すことにする(ついでに書いておくとこのように詳細の隠されたデータ型を抽象データ型という)。隠すには単にシグネチャに書かなければいい。具体的にはデータ型定義の = 以降の部分と,delete関数の定義部分だ。

シグネチャを定義するには module type宣言を使う。

# module type TABLE1 =
sig
type ('a, 'b) t
val empty : ('a, 'b) t
val add : 'a -> 'b -> ('a, 'b) t -> ('a, 'b) t
val retrieve : 'a -> ('a, 'b) t -> 'b option
val dump : ('a, 'b) t -> ('a * 'b) list
end;;
module type TABLE1 =
sig
type ('a, 'b) t
val empty : ('a, 'b) t
val add : 'a -> 'b -> ('a, 'b) t -> ('a, 'b) t
val retrieve : 'a -> ('a, 'b) t -> 'b option
val dump : ('a, 'b) t -> ('a * 'b) list
end

このシグネチャを与えて新しい Table1モジュールを定義する。といっても実態はTableモジュールとおなじだ。

# module Table1 : TABLE1 = Table;;
module Table1 : TABLE1

これで実体は同じだけどシグネチャの違うモジュールができた。2つを比べてみよう。

# Table.empty;;
- : ('a, 'b) Table.t = Table.Empty
# Table1.empty;;
- : ('a, 'b) Table1.t = <abstr>

Table1の方はデータ型か <abstr> になっている。これは抽象データ型を表している。

# Table.retrieve;;
- : 'a -> ('a, 'b) Table.t -> 'b option = <fun>
# Table1.retrieve;;
- : 'a -> ('a, 'b) Table1.t -> 'b option = <fun>

retrieve関数には両方ともアクセスできる。

# Table.delete;;
- : 'a -> ('a, 'b) Table.t -> ('a, 'b) Table.t = <fun>
# Table1.delete;;
Characters 0-13:
Table1.delete;;
^^^^^^^^^^^^^
Unbound value Table1.delete

Table1.delete はダメ。

open宣言

モジュールの関数を使うときには[モジュール名].[関数名]とするけど,open宣言をすればモジュール名をつけなくても使えるようになる。

# map (fun x -> x * x) [1;2;3;4];;
Characters 0-3:
map (fun x -> x * x) [1;2;3;4];;
^^^
Unbound value map
# open List;;
# map (fun x -> x * x) [1;2;3;4];;
- : int list = [1; 4; 9; 16]

複数のモジュールをopenしたとき,同じ名前の関数がある場合には後からopenしたモジュールの関数だけが使えるようになる。たとえば,map関数は ListモジュールにもArrayモジュールにもあるけど,次のようにするとArrayモジュールのほうだけが使える。

# open List;;
# open Array;;
# map (fun x -> x * 10) [|1;2;3;4|];;
- : int array = [|10; 20; 30; 40|]

↑Arrayのmapは使える。↓Listのmapは使えない。

# map (fun x -> x * 10) [1;2;3;4];;
Characters 22-31:
map (fun x -> x * 10) [1;2;3;4];;
^^^^^^^^^
This expression has type 'a list but is here used with type int array

モジュール名をつけてやれば使える。

# List.map (fun x -> x * 10) [1;2;3;4];;
- : int list = [10; 20; 30; 40]

FizzBuzz

出力も覚えたし書いてみた。

let fizzbuzz x =
let f = if x mod 3 = 0 then "Fizz" else "" in
let b = if x mod 5 = 0 then "Buzz" else "" in
let fb = f ^ b in
if fb = "" then string_of_int x else fb
;;
let () = for i = 1 to 100 do
print_endline (fizzbuzz i)
done
;;

実行結果は省略するけど,うまくいったとだけ書いておく。

練習問題8.6

本文中の関数 whle を参考にして,for式相当の機能を実現する再帰関数を定義しなさい。

こんなんでいいのかな。

# let rec fr frm t body =
if frm <= t then
begin body frm; fr (frm + 1) t body end
;;
val fr : int -> int -> (int -> 'a) -> unit = <fun>
# fr 1 5 (fun x -> print_endline ( string_of_int x));;
1
2
3
4
5
- : unit = ()

練習問題8.1

本文中でもふれたように,ref型は以下のように定義された,1フィールドの書き換え可能な レコードです。

type 'a ref = { mutable contents : 'a };; 

関数 ref,前置演算子 !,中置演算子 := の定義をレコードに関連した操作で書きなさい。

こうかな。

let ref x = { contents = x };;
let (!) x = x.contents;;
let (:=) x v = x.contents <- v;;

練習問題8.2

与えられた参照の指す先の整数を1増やす関数 incr を定義しなさい。

# let incr x = x := !x + 1;;
val incr : int ref -> unit = <fun>
# let x = ref 3;;
val x : int ref = {contents = 3}
# incr x;;
- : unit = ()
# !x;;
- : int = 4

練習問題8.4

参照と繰り返しの構文(while,for)を使ってフィボナッチ数を求める関数を定義しなさい。

# let fib n =
let fibs = ref (1, 1) in
let i = ref 1 in
while !i < n do
fibs := (snd !fibs, fst !fibs + snd !fibs);
i := !i + 1
done;
snd !fibs
;;
val fib : int -> int = <fun>

初項を第0項とした。

# fib 0;;
- : int = 1
# fib 1;;
- : int = 1
# fib 2;;
- : int = 2
# fib 3;;
- : int = 3
# fib 4;;
- : int = 5
# fib 5;;
- : int = 8
# fib 6;;
- : int = 13

チャネルを使った入出力

チャネルっていうのは,ファイルディスクリプタみたいなものだと思っておけば良さそう。

入力用には open_in と close_in を使う。

こういうファイル members.txt があったとして:

^o^ >type members.txt
andy
bill
charlie

ファイルから入力する例。

# let infile = open_in "C:/home/takatoh/members.txt";;
val infile : in_channel = <abstr>
# input_line infile;;
- : string = "andy"
# input_line infile;;
- : string = "bill"
# input_char infile;;
- : char = 'c'
# input_byte infile;;
- : int = 104

ファイルの最後に到達すると End_of_file 例外が発生する。

# input_line infile;;
- : string = "arlie"
# input_line infile;;
Exception: End_of_file.
# close_in infile;;
- : unit = ()

一方,ファイルに出力するには open_out と close_out。

# let outfile = open_out "C:/home/takatoh/foo.txt";;
val outfile : out_channel = <abstr>
# output_string outfile "Hello, world.";;
- : unit = ()
# output_char outfile '\n';;
- : unit = ()
# close_out outfile;;
- : unit = ()

できたファイル:

^o^ >type foo.txt
Hello, world.

まとめ

  入力 出力
オープン open_in open_out
クローズ close_in close_out
1行ずつ input_line output_string
1文字ずつ input_char output_char
1バイトずつ input_byte output_byte

ファイルに追加出力する

open_out を使ってファイルを開くと,そのファイルがすでに存在した場合,中身を消去してしまう。既存のファイルに追加するには,open_out_gen を使ってチャネルを作る*1

# let outfile = open_out_gen [Open_wronly; Open_append; Open_text] 0o666 "C:/home/takatoh/foo.txt";;
val outfile : out_channel = <abstr>

第1引数は open_flag といって,ファイルを開くときのオプション。上で設定してるのはライトオンリー,追加,テキストファイル,というところだろう。第2引数はファイルのパーミッション,第3引数はファイル名。で,かえってくるのは open_out と同じ出力用のチャネル(out_channel)。

# output_string outfile "foo\n";;
- : unit = ()
# output_string outfile "bar\n";;
- : unit = ()
# close_out outfile;;
- : unit = ()
# open_out_gen;;
- : open_flag list -> int -> string -> out_channel = <fun>

foo.txt はこうなる。

^o^ >type foo.txt
Hello, world.
foo
bar

ちゃんと追加になっている。

open_flag はヴァリアントで,The Objective Caml system release 3.1019.2 Module Pervasives: the initially opened module の General output functionsの項によると:

type open_flag =
| 	Open_rdonly 	(*	open for reading.	*)
| 	Open_wronly 	(*	open for writing.	*)
| 	Open_append 	(*	open for appending: always write at end of file.	 *)
| 	Open_creat 	(*	create the file if it does not exist.	*)
| 	Open_trunc 	(*	empty the file if it already exists.	*)
| 	Open_excl 	(*	fail if Open_creat and the file already exists.	*)
| 	Open_binary 	(*	open in binary mode (no conversion).	*)
| 	Open_text 	(*	open in text mode (may perform conversions).	*)
| 	Open_nonblock 	(*	open in non-blocking mode.	*)

*1:ここ,「プログラム in OCaml」に誤植あり。○ Open_wronly,× Open_wonly