M.Hiroi's Home Page

お気楽 OCaml プログラミング入門

集合 (3)


Copyright (C) 2008-2020 Makoto Hiroi
All rights reserved.

はじめに

今回は集合を例題にして、モジュール (ファンクタ) とオブジェクト指向を使ったプログラムを作りましょう。

●モジュール内でクラスを定義する

クラスはモジュール (ファンクタ) の中でも定義することもできます。たとえば、ファンクタ MakeSet の中でクラス set を定義すると、次のようになります。

リスト 9 : ファンクタ内でのクラス定義

(* ストラクチャ *)
module type ITEMTYPE =
  sig
    type t
    val compare : t -> t -> int
  end

(* ファンクタ *)
module MakeSet(Item: ITEMTYPE) :
  sig
    type t = Item.t
    class set :
      object ('a)
        method copy : 'a
        method delete : t -> unit
        method difference : 'a -> 'a
        method insert : t -> unit
        method intersection : 'a -> 'a
        method is_equal : 'a -> bool
        method is_subset : 'a -> bool
        method iter : (t -> unit) -> unit
        method length : int
        method member : t -> bool
        method union : 'a -> 'a
      end
  end
=
  struct
    type t = Item.t
    class set = 
      object(self : 'self_type)
        val mutable content = ([]: t list)
        val mutable size = 0

        method member p =
          let rec mem_eq = function
            [] -> false
          | x::xs -> if Item.compare p x = 0 then true else mem_eq xs
          in
            mem_eq content

        method insert p =
          if not (self#member p) then begin
            size <- size + 1;
            content <- p::content
          end else ()

        method delete p =
          if self#member p then 
            (content <- List.filter (fun x -> Item.compare p x <> 0) content;
             size <- size - 1)
          else raise Not_found

        ... 省略 ...
    end
  end

シグネチャでは "class クラス名 : object ... end" でクラス型を定義します。シグネチャでデータ型 t を宣言しているので、t を使ってクラス型を定義します。ファンクタ本体ではデータ型 t を Item.t に定義し、それを使ってクラス set を定義します。要素の比較はストラクチャ Item の関数 compare を使います。あとは今までのプログラムと同じです。

簡単な例として、整数値 (int) を格納する集合 IntSet を作ってみましょう。IntSet を定義すると次のように表示されます。

# module IntSet = MakeSet(struct type t = int let compare x y = x - y end)
module IntSet :
  sig
    type t = int
    class set :
      object ('a)
        method copy : 'a
        method delete : t -> unit
        method difference : 'a -> 'a
        method insert : t -> unit
        method intersection : 'a -> 'a
        method is_equal : 'a -> bool
        method is_subset : 'a -> bool
        method iter : (t -> unit) -> unit
        method length : int
        method member : t -> bool
        method union : 'a -> 'a
      end
  end

それでは実際に試してみましょう。

# open IntSet;;
# let print s = s#iter (fun x -> print_int x#get; print_string " ");;
val print : < iter : (int -> unit) -> 'a; .. > -> 'a = <fun>
# let a = new set compare;;
val a : IntSet.set = <obj>
# for i = 1 to 5 do a#insert i done;;
- : unit = ()
# print a;;
5 4 3 2 1 - : unit = ()
# let b = new set;;
val b : IntSet.set = <obj>
# for i = 4 to 8 do b#insert i done;;
- : unit = ()
# print b;;
8 7 6 5 4 - : unit = ()
# print (a#union b);;
1 2 3 8 7 6 5 4 - : unit = ()
# print (a#intersection b);;
4 5 - : unit = ()
# print (a#difference b);;
1 2 3 - : unit = ()

このようにファンクタとクラスを組み合わせることも簡単にできます。

●オブジェクトを格納する集合

ところで、拙作のページ「集合」で作成したクラス set は、要素を比較する関数をクラスの引数に渡しました。ここでは異なる方法を試してみましょう。集合に格納する要素をオブジェクトとし、そのオブジェクトに要素同士を比較するメソッド compare を定義することにします。

それではプログラムを作りましょう。最初にクラスを定義します。

リスト 10 : クラス set の定義

class ['a] set = 
  object(self: 'self_type)
    val mutable content = ([]: 'a list)
    val mutable size = 0

    method member p =
      let rec mem_eq = function
        [] -> false
      | x::xs -> if p#compare x = 0 then true else mem_eq xs
      in
        mem_eq content

    method insert p =
      if not (self#member p) then begin
        size <- size + 1;
        content <- p::content
      end else ()

    method delete p =
      if self#member p then 
        (content <- List.filter (fun x -> p#compare x <> 0) content;
         size <- size - 1)
      else raise Not_found

    ... 省略 ...
  end

今回はオブジェクトのメソッド compare を使ってプログラムを作るので、型推論により型変数 'a はあるオブジェクトの型に制限されます。これはあとで説明します。要素の比較はオブジェクトのメソッド compare を使います。compare の仕様は OCaml の関数 compare と同じです。プログラムは compare p x を p#compare x と書き換えるだけです。

●constraint

実際に set を定義すると次のように表示されます。

class ['a] set :
  object ('b)
    constraint 'a = < compare : 'a -> int; .. >
    val mutable content : 'a list
    val mutable size : int
    method copy : 'b
    method delete : 'a -> unit
    method difference : 'b -> 'b
    method insert : 'a -> unit
    method intersection : 'b -> 'b
    method is_equal : 'b -> bool
    method is_subset : 'b -> bool
    method iter : ('a -> unit) -> unit
    method length : int
    method member : 'a -> bool
    method union : 'b -> 'b
  end

constraint は「制約」という意味で、集合の要素 'a はメソッド compare を持つオブジェクトでなければならないことを表しています。今回は OCaml の型推論にお任せしましたが、プログラムで constraint を記述することもできます。

●実行例

それでは簡単な実行例を示しましょう。まず最初に、集合に格納するオブジェクトを定義します。

リスト 11 : 要素となるオブジェクト

class ['a] item (x: 'a) =
  object(self: 'self_type)
    val n = x
    method get = n
    method compare (x: 'self_type) = compare n x#get
  end

データの比較は関数 compare で行います。オブジェクト以外のデータ型であれば、これで大小関係を比較することができます。

実行例は次のようになります。

# let print s = s#iter (fun x -> print_int x#get; print_string " ");;
val print : < iter : (< get : int; .. > -> unit) -> 'a; .. > -> 'a = <fun>
# let a = new set;;
val a : (< compare : 'a -> int; _.. > as 'a) set = <obj>
# for i = 1 to 5 do a#insert (new item i) done;;
- : unit = ()
# print a;;
5 4 3 2 1 - : unit = ()
# let b = new set;;
val b : (< compare : 'a -> int; _.. > as 'a) set = <obj>
# for i = 4 to 8 do b#insert (new item i) done;;
- : unit = ()
# print b;;
8 7 6 5 4 - : unit = ()
# print (a#union b);;
1 2 3 8 7 6 5 4 - : unit = ()
# print (a#intersection b);;
4 5 - : unit = ()
# print (a#difference b);;
1 2 3 - : unit = ()

最初に集合の要素を表示する関数 print を定義します。型推論により、引数 s はメソッド iter を持つオブジェクトで、iter に渡す関数の引数はメソッド get を持つオブジェクトであることが示されています。

次に、集合 a と b を生成します。a は {1, 2, 3, 4. 5} で、b は {4, 5, 6, 7, 8} です。集合演算を行うと、a と b の和は {1, 2, 3, 4, 5, 6, 7, 8} になり、積は {4, 5} になり、差は {1, 2, 3} になります。

このように、格納するオブジェクトのメソッド compare を使って集合を表すクラスを定義することもできます。

●ファンクタ MakeSet でオブジェクトを格納する

ところで、ファンクタで定義した集合でも、オブジェクトを格納することができます。次の例を見てください。

# module IntObjSet = MakeSet(struct type t = int item
  let compare x y = x#compare y end);;
module IntObjSet :
  sig
    type set
    val create : unit -> set
    val member : int item -> set -> bool
    val insert : int item -> set -> set
    val delete : int item -> set -> set
    val union : set -> set -> set
    val intersection : set -> set -> set
    val difference : set -> set -> set
    val is_subset : set -> set -> bool
    val is_equal : set -> set -> bool
    val iter : (int item -> unit) -> set -> unit
    val set_of_list : int item list -> set
    val list_of_set : set -> int item list
  end
# open IntObjSet;;
# let print s = iter (fun x -> print_int x#get; print_string " ") s;;
val print : IntObjSet.set -> unit = <fun>
# let a = set_of_list (List.map (fun x -> new item x) [1;2;3;4;5]);;
val a = IntObjSet.set = <abstr>
# print a;;
1 2 3 4 5 - : unit = ()
# let b = set_of_list (List.map (fun x -> new item x) [4;5;6;7;8]);;
val b = IntObjSet.set = <abstr>
# print b;;
4 5 6 7 8 - : unit = ()
# print (union a b);;
1 2 3 4 5 6 7 8 - : unit = ()
# print (intersection a b);;
4 5 - : unit = ()
# print (difference a b);;
1 2 3 - : unit = ()

ファンクタに渡すストラクチャで、type t = int item とし、compare x y = x#compare x とすれば、オブジェクト item を格納するモジュール IntObjSet を生成することができます。このように、OCaml にはモジュール (ファンクタ) という優れた機能があるので、複数の要素を格納するデータ構造 (コレクション) をクラスで実装することは少ないのかもしれませんね。

●問題

可変長配列を操作するモジュール Arraylist をクラスを使わないで定義してください。













●解答

リスト : モジュール Arraylist

module Arraylist :
sig
  type 'a arraylist
  val create : int -> 'a -> 'a arraylist
  val push : 'a arraylist -> 'a -> unit
  val pop : 'a arraylist -> 'a
  val peek : 'a arraylist -> 'a
  val get : 'a arraylist -> int -> 'a
  val set : 'a arraylist -> int -> 'a -> unit
  val length : 'a arraylist -> int
  val find_if : ('a -> bool) -> 'a arraylist -> 'a option
  val position_if : ('a -> bool) -> 'a arraylist -> int
  val count_if : ('a -> bool) -> 'a arraylist -> int
  val map : ('a -> 'b) -> 'a arraylist -> 'b arraylist
  val filter : ('a -> bool) -> 'a arraylist -> 'a arraylist
  val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b arraylist -> 'a
  val iter : ('a -> 'b) -> 'a arraylist -> unit
end = struct
  type 'a arraylist = {mutable buff : 'a array; mutable top : int}

  let create init_size init_value =
    {buff = Array.make init_size init_value; top = 0}

  let push xs x =
    if xs.top >= Array.length xs.buff then
      let newbuff = Array.make (2 * Array.length xs.buff) xs.buff.(0) in
      Array.blit xs.buff 0 newbuff 0 (Array.length xs.buff);
      xs.buff <- newbuff
    else ();
    xs.buff.(xs.top) <- x;
    xs.top <- xs.top + 1

  let pop xs =
    if xs.top = 0 then raise Empty
    else (xs.top <- xs.top - 1; xs.buff.(xs.top))

  let peek xs =
    if xs.top = 0 then raise Empty
    else xs.buff.(xs.top - 1)

  let get xs i =
    if i < 0 || i >= xs.top then raise Out_of_range
    else xs.buff.(i)

  let set xs i x =
    if i < 0 || i >= xs.top then raise Out_of_range
    else xs.buff.(i) <- x

  let length xs = xs.top

  (* 高階関数 *)
  let find_if pred xs =
    let rec _find i =
      if i >= xs.top then None
      else if pred xs.buff.(i) then Some xs.buff.(i)
      else _find (i + 1)
    in _find 0

  let position_if pred xs =
    let rec _position i =
      if i >= xs.top then -1
      else if pred xs.buff.(i) then i
      else _position (i + 1)
    in _position 0

  let count_if pred xs =
    let rec _count i c =
      if i >= xs.top then c
      else _count (i + 1) (if pred xs.buff.(i) then c + 1 else c)
    in _count 0 0

  let map f xs =
    let ys = create xs.top (f xs.buff.(0)) in
    ys.top <- 1;
    for i = 1 to (xs.top - 1) do
      push ys (f xs.buff.(i))
    done;
    ys

  let filter pred xs =
    let ys = create xs.top xs.buff.(0) in
    for i = 0 to (xs.top - 1) do
      if pred xs.buff.(i) then push ys xs.buff.(i) else ()
    done;
    ys

  let fold_left f a xs =
    let rec _fold i a =
      if i >= xs.top then a
      else _fold (i + 1) (f a xs.buff.(i))
    in _fold 0 a

  let iter f xs =
    let rec _iter i =
      if i >= xs.top then ()
      else (f xs.buff.(i); _iter (i + 1))
    in _iter 0
end
# open Arraylist;;
# let a = create 4 0;;
val a : int Arraylist.arraylist = <abstr>
# for i = 1 to 8 do push a i done;;
- : unit = ()
# length a;;
- : int = 8
# for i = 0 to 7 do print_int (get a i); print_newline() done;;
1
2
3
4
5
6
7
8
- : unit = ()
# for i = 0 to 7 do set a i ((get a i) * 10) done;;
- : unit = ()
# iter (fun x -> print_int x; print_newline()) a;;
10
20
30
40
50
60
70
80
- : unit = ()
# iter (fun x -> print_int x; print_newline()) b;;
100
400
900
1600
2500
3600
4900
6400
# let c = filter (fun x -> x mod 20 = 0) a;;
val c : int Arraylist.arraylist = <abstr>
# iter (fun x -> print_int x; print_newline()) c;;
20
40
60
80
- : unit = ()
# fold_left (+) 0 a;;
- : int = 360
# fold_left (+) 0 b;;
- : int = 20400
# find_if (fun x -> x > 50) a;;
- : int option = Some 60
# find_if (fun x -> x > 100) a;;
- : int option = None
# position_if (fun x -> x > 50) a;;
- : int = 5
# position_if (fun x -> x > 100) a;;
- : int = -1
# count_if (fun x -> x > 50) a;;
- : int = 3
# count_if (fun x -> x < 50) a;;
- : int = 4
# count_if (fun x -> x < 0) a;;
- : int = 0
# for i = 0 to (length a - 1) do print_int (pop a); print_newline() done;;
80
70
60
50
40
30
20
10
- : unit = ()
# length a;;
- : int = 0

初版 2008 年 7 月 27 日
改訂 2020 年 7 月 19 日