M.Hiroi's Home Page

Functional Programming

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

[ PrevPage | OCaml | NextPage ]

オブジェクト指向

プログラミングに興味のある方ならば、「オブジェクト指向」という言葉は聞いたことがあると思います。よく使われているオブジェクト指向言語に C++ や Java があります。また、Lightweight Language と呼ばれているプログラミング言語、たとえば Perl, Python, Ruby, JavaScript などはオブジェクト指向をサポートしています。

多くの言語でサポートされている「オブジェクト指向」ですが、関数型言語では Common Lisp の CLOS (Common Lisp Object System) が有名でしょう。CLOS は Smalltalk, C++, Java などのポピュラーなオブジェクト指向とはちょっと違っていて、興味深い機能がたくさんあります。

OCaml は "Objective Caml" の略なので、もちろんオブジェクト指向をサポートしています。ただし、OCaml には多相性と型推論があるため、他のオブジェクト指向言語とは異なる部分がかなりあります。とくにオブジェクトの「型」については、一般的なオブジェクト指向とは大きく異なります。

OCaml 初心者である M.Hiroi が OCaml のオブジェクト指向を説明するのは無謀なことだと思いますが、簡単なところから少しずつ勉強していきましょう。まず最初に、一般的なオブジェクト指向について簡単に説明します。

なお、この説明は拙作のページ Lightweight Language お気楽 Python プログラミング入門第 5 回 と同じです。既に読んだことがある方や、一般的なオブジェクト指向について理解されている方は、読み飛ばしてもらってもかまいません。

次へ

●オブジェクトとは?

プログラムを作る場合、全体を小さな処理に分割して、ひとつひとつの処理を作成し、それらを組み合わせて全体のプログラムを完成させます。このとき、基本的な部品となるのが関数です。つまり、処理を関数単位で分割して、それらを組み合わせてプログラムを作るわけです。もともと関数の役割は、入力されたデータを処理してその結果を返すことです。つまり、関数は機能を表しているのです。このため、全体を小さな処理に分割するにしても、機能単位で行われることが普通です。

オブジェクト指向プログラミングでは、関数ではなく「オブジェクト (object)」を部品として扱います。たとえば、えんぴつを考えてみましょう。えんぴつには、色、長さ、固さ、などいろいろな性質があります。そして、えんぴつを使って文字を書いたり、絵を描いたりすることができます。プログラムでは、このような性質をデータで表し、機能を関数で表すことになります。そしてオブジェクトとは、このデータと関数を結び付けたものなのです。

いままでのプログラミング言語では、データと関数を別々に定義するため、それをひとつのオブジェクトとして表すことができません。えんぴつで文字を書くにも、えんぴつの種類をチェックして文字を書くようにプログラムしなければいけません。ところが、オブジェクトはデータと関数を結び付けたものなので、自分がなにをしたらよいかわかっています。えんぴつオブジェクトに文字を書けと命じれば、それが赤えんぴつのオブジェクトであれば文字は赤に、黒えんぴつのオブジェクトであれば黒い文字になるのです。

このように、オブジェクトはデータと関数をひとつにまとめたものです。従来のプログラミングが全体を機能単位で分割するのに対し、オブジェクト指向プログラミングでは全体をオブジェクト単位に分割して、それを組み合わせることでプログラムを作成します。

ところで、データと関数を結び付けることは、従来のプログラミング言語でも可能です。オブジェクト指向はプログラミングの考え方のひとつであり、C++ のようなオブジェクト指向言語を使わなくても、たとえばC言語でもその考え方にしたがってプログラムを作れば、オブジェクト指向プログラミングになります。

実際、オブジェクト指向には様々な考え方があり、いろいろなオブジェクト指向言語が存在します。ですが、データと関数をひとつにまとめたものをオブジェクトとして扱うという基本的な考え方は、オブジェクト指向言語の元祖と言われる Smalltalk でも、C++, Java, OCaml でも同じです。

●クラスとインスタンス

次は、一般的なオブジェクト指向機能について簡単に説明します。

「クラス (class)」はオブジェクトの振る舞いを定義したものです。ここでデータを格納するための変数や、それを操作する関数が定義されます。この変数をメンバ変数とかインスタンス変数といい、関数を「メソッド (method)」といいます。メソッドはあとで説明します。

クラスはオブジェクトの設計図にあたるもので、オブジェクトの「雛形」と呼ぶこともあります。クラスはオブジェクトの振る舞いを定義するだけで、アクセスできる実体はなにも生み出していない、ということに注意してください。

このクラスから実体として作り出されるのが「インスタンス (instance)」です。このインスタンスを「オブジェクト」と考えてください。インスタンスを生成する方法は、当然ですがプログラミング言語によって違います。たとえば C++ や Java は new を使います。図 1 を見てください。


               図 1 : クラスとインスタンスの関係

クラスはオブジェクトの定義を表すものですから、Foo というクラスはひとつしかありません。これに対し、インスタンスはクラスから生み出されるオブジェクトです。たとえば、クラス Foo に new を適用することで、いくつでもインスタンスを生み出すことができるのです。クラスは設計図であり、それに従って作られるオブジェクトがインスタンスと考えるとわかりやすいでしょう。

●メソッド

メソッドはオブジェクトと結びついた関数です。オブジェクト指向プログラミングでは、ほかの関数から直接オブジェクトを操作することはせず、メソッドを呼び出すことで行います。メソッドは、クラスが異なっていれば同じ名前のメソッドを定義することができます。たとえば、クラス Foo1 にメソッド bar() が定義されていても、クラス Foo2 に同名のメソッド bar() を定義することができます。

そして、ここからが重要なのですが、あるオブジェクトに対してメソッド bar() を呼び出した場合、それが Foo1 から作られたオブジェクトであれば、Foo1 で定義された bar() が実行され、Foo2 から作られたオブジェクトであれば、Foo2 で定義された bar() が実行されるのです。

このように、オブジェクトが属するクラスによって、実行されるメソッドが異なるのです。この機能を「ポリモーフィズム(polymorphism)」と呼びます。これにより、オブジェクトは自分が行うべき適切な処理を実行できるわけです。

クラス、インスタンス、メソッドの関係は図 2 のようになります。


         図 2 : クラス、インスタンス、メソッドの関係

クラスという設計図が中心にあり、そこからインスタンスが生み出され、メソッドを使ってインスタンスを操作する、という関係になります。

●OCaml のクラス

さて、一般的な話はここまでにして、OCaml のオブジェクト指向機能に目を向けてみましょう。OCaml は class 宣言でクラスを定義します。class の構文を示します。

class class_name = object ... end

class の次にクラス名を指定し、= の右辺 object ... end でインスタンス変数やメソッドを定義します。

一番簡単なクラス定義を示しましょう。次の例を見てください。

# class foo = object end;;
class foo : object end

一般的なオブジェクト指向言語では、クラス名を英大文字から始めることが多いのですが、OCaml の場合、英大文字から始まる名前はコンストラクタとして認識されるので、クラス名は英小文字から始めます。foo はクラス名しかありませんが、これでも立派なクラスなのです。次の例を見てください。

# let a = new foo;;
val a : foo = <obj>

OCaml は次の形式でインスタンスを生成します。

new クラス名

インスタンスを生成するとき、引数を指定することができます。これはあとで説明します。

●インスタンス変数とメソッドの定義

OCaml の場合、インスタンス変数は val で、メソッドは method で定義します。

val (mutable) 変数名 = 初期値
method メソッド名 引数 ... = 式

OCaml の場合、インスタンス変数にアクセスできるのはクラス内のメソッドだけです。クラスの外からアクセスすることはできません。また、名前の前に mutable を付けると値を書き換えることができます。インスタンス変数の書き換えは次の式で行います。

インスタンス変数名 <- 式

メソッドの定義は let の代わりに method を使います。引数がないメソッドを定義するとき、( ) をつける必要はありません。

それでは、簡単な例を示しましょう。

リスト 1 : インスタンス変数とメソッドの定義

class foo = object
  val mutable a = 10
  method get_a = a
  method set_a x = a <- x
end

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

class foo :
  object
    val mutable a : int
    method get_a : int
    method set_a : int -> unit
  end

メソッドの呼び出しは次の形式で行います。

式#メソッド名

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

# let x = new foo;;
val x : foo = <obj>
# x#get_a;;
- : int = 10
# x#set_a 100;;
- : unit = ()
# x#get_a;;
100

このように、メソッドを呼び出してインスタンス変数の値を取得したり、値を書き換えることができます。

●オブジェクトの初期化

OCaml は new でオブジェクトを生成するとき、関数の引数のようにデータを渡すことができます。次の構文を見てください。

class クラス名 引数1 引数2 ... = object ... end

クラス名と = の間に引数を指定します。これで object ... end の中から与えられた引数を参照することができます。たとえば、クラス foo に整数値を渡す場合は次のようになります。

リスト 2 : 引数の指定方法

class foo (init: int) = object
  val mutable a = init
  method get_a = a
  method set_a x = a <- x
end

引数 init でインスタンス変数の値を初期化します。init の型は int に指定します。このプログラムでは引数のデータ型を指定しましたが、OCaml はクラスに型変数を渡すことで多相的なクラスを定義することができます。これはあとで説明します。

実際にクラス foo を定義すると次のように表示されます。

class foo :
  int ->
  object
    val mutable a : int
    method get_a : int
    method set_a : int -> unit
  end

あとは new でインスタンスを生成するときに値を渡すだけです。

# let a = new foo 100;;
val a : foo = <obj>
# a#get_a;;
- : int = 100
# let b = new foo;;
val b : int -> foo = <fun>
# let c = b 1000;;
val c : foo = <obj>
# c#get_a;;
- : int = 1000

最初の例のように、引数を与えるとインスタンスが生成されます。引数を与えない場合、引数を受け取ってインスタンスを返す関数が生成されます。その関数を使ってインスタンスを生成することもできます。

●多相クラス

クラスはヴァリアントやレコードと同様に、型変数を使って多相的なデータ構造を定義することができます。これを「多相クラス (polymorphic class)」といいます。型変数は次のように指定します。

class [型変数1, 型変数2, ..., 型変数n] クラス名 = object ... end

クラス名の前の角カッコ [ ] に型変数を指定します。そして、object ... end の中で型変数を参照することができます。

それでは簡単な例題としてスタックを作ってみましょう。クラスでスタックを定義すると次のようになります。

リスト 3 : スタック

(* 例外 *)
exception Empty

(* クラス定義 *)
class ['a] stack = object
  (* データを格納するリスト *)
  val mutable content = ([]: 'a list)

  (* データを追加 *)
  method push x =
    content <- x::content

  (* データの削除 *)
  method pop =
    match content with
      [] -> raise Empty
    | x::xs -> content <- xs; x

  (* データの取得 *)
  method top =
    match content with
      [] -> raise Empty
    | x::_ -> x

  (* スタックは空か *)
  method is_empty = content = []
end

データはインスタンス変数 content のリストに格納します。ここでクラスに渡された型変数 'a を使ってデータ型を 'a list に指定します。データ型を指定しなかったり、型変数を渡さずに 'a list と指定するとコンパイルでエラーになります。

メソッドの動作は拙作のページ モジュール で作成した「参照型データを使ったスタックの実装」と同じです。メソッド push は content の先頭にデータ x を追加します。メソッド pop は content の先頭の要素を取り除いて、その値を返します。メソッド top は content の先頭の要素を返します。メソッド is_empty は content が空リストであれば true を返します。

実際に、クラス stack を定義すると次のように表示されます。

class ['a] stack :
  object
    val mutable content : 'a list
    method is_empty : bool
    method pop : 'a
    method push : 'a -> unit
    method top : 'a
  end

簡単な実行例を示します。

# let a = new stack;;
val a: '_a stack = <obj>
# for i = 0 to 9 do a#push i done;;
- : unit = ()
# a#is_empty;;
- : bool = false
# a#top;;
- : int = 9
# while not a#is_empty do Printf.printf "%d " a#pop done;;
9 8 7 6 5 4 3 2 1 0 - : unit = ()

このように、型変数を渡すことで多相クラスを定義することができます。

●private メソッド

もう一つ簡単な例としてキューを作ってみましょう。拙作のページ モジュール で作成した「キュー」をクラスを使って実装します。次のリストを見てください。

リスト 4 : キュー

exception Empty

class ['a] queue =
  object (self)
    val mutable front = ([]: 'a list)
    val mutable rear  = ([]: 'a list)

    (* データの移動 *)
    method private move =
      front <- (List.rev rear); rear <- []

    (* データの追加 *)
    method enqueue x = rear <- x :: rear

    (* データの削除 *)
    method dequeue =
      if front = [] then self#move;
      match front with
        [] -> raise Empty
      | x::xs -> front <- xs; x

    (* データの取得 *)
    method top = 
      if front = [] then self#move;
      match front with
        [] -> raise Empty
      | x::_ -> x

    (* キューは空か *)
    method is_empty = front = [] && rear = []
  end

インスタンス変数 front と rear にデータを格納するリストをセットします。このプログラムは front と rear の値を書き換えることで動作します。データの追加は簡単ですね。メソッド enqueue は引数 x を rear の先頭に追加するだけです。

メソッド dequeue と top は、rear のデータを front に移動するメソッド move を呼び出すと簡単に定義できます。メソッドから同じクラスのメソッドを呼び出す場合、メソッドを呼び出したインスタンスが必要になります。OCaml の場合、次の構文で呼び出し元のインスタンスに名前を付けて、それを用いてメソッドを呼び出すことができます。

object (変数名) ... end 

今回は変数名に self を使っています。self#move とすれば、メソッド move を呼び出して、rear のデータを front に移動することができます。

ここで move の前に付いているキーワード private に注目してください。move はクラス内部のメソッドから呼び出される作業用のメソッドです。もしも外部から move を呼び出すことができると、キューの状態を破壊することになるのでとても危険です。このような場合、private をつけることでメソッドを外部から隠蔽することができます。

dequeue と top は front が空リストであれば move を呼び出してデータを移動します。そして、dequeue はキューから先頭の要素を取り除き、top は先頭の要素を返します。is_empty はキューが空の場合は true を返します。

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

class ['a] queue :
  object
    val mutable front : 'a list
    val mutable rear : 'a list
    method dequeue : 'a
    method enqueue : 'a -> unit
    method is_empty : bool
    method private move : unit
    method top : 'a
  end

メソッド move のデータ型が表示されていますが、private が付いているので外部から呼び出すことはできません。

それでは簡単な実行例を示します。

# let a = new queue;
val a : '_a queue = <obj>
# for i = 0 to 9 do a#enqueue i done;;
- : unit = ()
# a#is_empty;;
- : bool = false
# while not a#is_empty do Printf.printf "%d " a#dequeue done;;
0 1 2 3 4 5 6 7 8 9 - : unit = ()

正常に動作していますね。

●オブジェクトの型

さて、これまでに説明した OCaml のクラス、インスタンス、メソッドの働きは、多相クラスを除くと一般的なオブジェクト指向とそれほど変わらないように思えます。class 文でクラスを定義し、new を適用してインスタンスを生成する方法は、一般的なオブジェクト指向とまったく同じです。ところが、OCaml のオブジェクト指向はクラスを定義しなくてもインスタンスを生成することができます。次の例を見てください。

# let a = object
  val mutable a = 10
  method get_a = a
  medhod set_a x = a <- x
  end
val a : < get_a : int; set_a : int -> unit > = <obj>
# a#get_a;;
10
# a#set_a 100;;
- : unit = ()
# a#get_a;;
100

OCaml は object ... end でインスタンス (オブジェクト) を生成することができます。ちなみに、class 文によるクラス定義は次の式と同じ意味です。

class クラス名 = fun 引数1 引数2 ... -> object ... end

ここで表示されるデータ型に注目してください。< > の中にメソッドのデータ型が列挙されていますね。これがオブジェクトのデータ型を表しています。OCaml の場合、オブジェクトの型は次の形式で表します。

< メソッド名1: 型1; メソッド名2: 型2; ... ; メソッド名n: 型n >

オブジェクトの型にインスタンス変数は関係ありません。インスタンス変数の名前が異なっていても、メソッドの名前と型が一致すれば、同じオブジェクトの型と判断されます。OCaml の場合、インスタンス変数にアクセスできるのはメソッドだけなので、メソッド名とその型を使ってオブジェクトの型を表すことができるわけです。

したがって、異なるクラスから生成されたオブジェクトでも、同一のオブジェクト型と判断されることがあります。つまり、OCaml におけるクラスはデータ型そのものを表しているのではなく、オブジェクトのデータ型に「名前」を付けただけなのです。もちろん、名前をつけることには意味があり、オブジェクトの型をそのまま使うよりも、クラス名を使うことでわかりやすいプログラムを作ることができます。

●OCaml の部分型

M.Hiroi は「クラス=型」という意識を強く持っていたので、「クラス≠型」という OCaml のオブジェクト指向には本当に驚きました。これで本当にオブジェクト指向が機能するのか不思議に思っていたのですが、実際に使ってみると多相性と型推論により、とても柔軟なシステムになっているのです。たとえば、次の例を見てください。

let foo obj = obj#get_a;;
val foo : < get_a : 'a; .. > -> 'a = <fun>

関数 foo はオブジェクト obj を受け取り、メソッド get_a を呼び出してその値を返します。OCaml は静的な型付けを行うプログラミング言語ですが、メソッドの選択はプログラムの実行時に行う「遅延束縛 (late binding)」を採用しているので、メソッド get_a が定義されていなくてもコンパイルすることが可能です。

また、遅延束縛により OCaml はメソッドのポリモーフィズムを実現しています。ただし、メソッドの呼び出しは、その分だけ関数呼び出しよりも少し遅くなります。

ここで obj の型に注目してください。引数 obj に対応するメソッド get_a を呼び出すので、当然ですが obj は型推論によりオブジェクトを表すデータ型になります。最後に現れている 2 つのピリオド ( .. ) はパターンマッチングのワイルドカードみたいなもので、ほかにメソッドがあってもよいことを表しています。次の例を見てください。

(1) < get_a : 'a >
(2) < get_a : 'a; get_b : 'b >
(3) < get_a : 'a: get_b : 'b; ... ; get_n : 'n >

どのオブジェクトの型もメソッド get_a があるので、関数 foo の引数として渡すことができます。OCaml の場合、(2) と (3) は (1) の「部分型 (subtypeing)」になります。データ型を集合とみなした場合、部分型はある型の部分集合を表していると考えることができます。

(1), (2), (3) はどれもメソッド get_a: 'a を持つデータ型です。これを一つの大きな集合と考えます。(2) はその中で get_b: 'b を持つデータ型の集合を表しているので、(2) は (1) の部分集合と考えることができます。どうように、(3) は (1) の部分集合で、(2) の部分集合にもなっています。

OCaml の部分型の規則を次に示します。

(a) < メソッド名1 : 型1; ... ; メソッド名m: 型m >
(b) < メソッド名1 : 型1; ... ; メソッド名m: 型m; ...; メソッド名n: 型n >
型 (b) は 型 (a) の部分型である

(b) は (a) と同じ型のメソッドをすべて含んでいて、そこにいくつかのメソッドを追加したものです。OCaml の場合、これが部分型になります。

一般的なオブジェクト指向では、クラスを「継承」することによって部分型が発生します。このように、継承を宣言することで部分型を生成する方法を「名前的部分型 (nomincal subtyping)」といいます。これに対し、OCaml の場合は継承に関係なく部分型が発生することがあります。これを「構造的部分型 (structural subtyping)」といいます。継承についてはあとで詳しく説明します。

関数 foo の場合、引数 obj の型は (1) の任意の部分型を表していると考えることができます。したがって、次のように部分型のオブジェクトであれば、関数 foo を呼び出すことができます。

# foo (object method get_a = 10 method get_b = 20 end);;
- : int = 10
# foo (object method get_a = 1.234 method get_b = 20 method get_c = 30 end);;
- : float = 1.234

メソッド get_a の型は型変数 'a なので、任意のデータ型に対応することができます。このように、関数 foo はオブジェクトにメソッド get_a があれば動作します。もちろん、OCaml は静的な型チェックを行うので、get_a を持たないオブジェクトを foo に渡すと、コンパイルでエラーとなります。

●ダック・タイピング

Python や Ruby など動的な型付けを行うプログラミング言語では、同じインターフェースが備わっているオブジェクトは同じデータ型とみなす、という手法 (考え方) があります。これを「ダック・タイピング」といい、よく用いられる手法のようです。ご参考までに、Ruby のプログラムを示します。

リスト 5 : ダック・タイピング (Ruby)

class Foo
  def initialize
    @a = 10
  end
  def get_a
    @a
  end
  def set_a(x)
    @a = x
  end
end

class Bar
  def initialize
    @a = 20
  end
  def get_a
    @a
  end
  def set_a(x)
    @a = x
  end
end

def foo(x)
  print x.get_a
end

foo(Foo.new)   # 10 と表示する
foo(Bar.new)   # 20 と表示する

クラス Foo と Bar は異なるクラスで継承関係もありません。この場合、Ruby は異なるデータ型と判断します。しかし、どちらのクラスにもメソッド get_a が定義されているので、関数 foo にインスタンスを渡して get_a を呼び出すことができます。動的なプログラミング言語では、このような「ダック・タイピング」を簡単に行うことができます。

OCaml は静的に強く型付けされる言語ですが、構造的部分型によりダック・タイピングのようなプログラミングスタイルも可能になっています。もちろん、コンパイル時に静的な型チェックが行われるので、エラーを検出することもできます。

●辞書 (mutable) の実装

最後に、クラスの簡単な例題として、モジュール Hashtbl のような mutable (変更可能) な辞書を作ってみましょう。辞書の操作メソッドを下表に示します。

表 : 辞書の操作メソッド
名前機能
d#add k v 辞書 d にキー k と値 v を追加する
d#mem k 辞書 d にキー k があれば真を返す
d#find k 最初に見つけたキー k の値を求める (k がなければ例外を送出)
d#find_opt k 最初に見つけたキー k の値を求める (返り値は option)
d#find_all k キー k と値 v の組を全て求める (返り値は list)
d#remove k 最初に見つけたキー k とその値を削除する
d#remove_all k キー k とその値を全て削除する
d#clear 辞書 d を空にする
d#length 辞書 d に含まれている要素の個数を返す
d#is_empty 辞書 d が空ならば真を返す
d#iter func 辞書 d の各要素に関数 func を適用する

メソッド add は辞書 d にキー k とその値 v を追加します。今回は同じキーを何個でも辞書に登録できるようにします。ただし、有効な値は直近に追加したキーの値とします。つまり、以前に登録された値は隠蔽されるわけです。メソッド mem, find, find_opt は直近に登録したキーを探索します。find_all はキー k と値の組をリストに格納して返します。

たとえば、キー "foo" と値 10 を登録し、次に "foo" と 20 を登録します。find "foo" は 20 を、find_opt "foo" は Some 20 を返します。メソッド remove は直近に登録したキー k とその値を削除します。remove "foo" は直近に登録した "foo" と 20 を削除します。このあと、"foo" を探索すると、最初に登録した値 10 が見つかります。remove_all はキー k とその値をすべて削除します。

メソッド length は辞書に格納されているキーと値の組の個数を返します。今回は同じキーを辞書に複数登録できるので、length の返り値はキーの種類の総数ではないことに注意してください。メソッド iter も同様です。たとえば、辞書に ("foo", 10) と ("foo", 20) が登録されているならば、これらの組に対して関数 func が呼び出されます。

●プログラムの作成

プログラムはモジュール List の「連想リスト」を使うと簡単です。次のリストを見てください。

リスト : 辞書 (連想リスト版)

class ['k, 'v] dic =
object
  val mutable xs = ([] : ('k * 'v) list)

  method add key value = xs <- (key, value)::xs
  method mem key = List.mem_assoc key xs
  method find key = List.assoc key xs
  method find_opt key = List.assoc_opt key xs
  method find_all key = List.filter (fun (k, _) -> k = key) xs
  method remove key = xs <- List.remove_assoc key xs
  method remove_all key = xs <- List.filter (fun (k, _) -> k <> key) xs
  method clear = xs <- []
  method length = List.length xs
  method iter f = List.iter f xs
  method is_empty = xs = []
end

クラス名は dic としました。型変数 'k がキーの型、'v が値の型を表します。インスタンス変数 xs に連想リストを格納します。型は ('k * 'v) list になります。それから、xs は mutable に設定することをお忘れなく。これで xs の値を更新することができます。

メソッド add は引数 k, v をタプルに格納し、それを xs の先頭に追加するだけです。連想リストの探索関数 mem_assoc, assoc_opt, assoc は連想リストを線形探索するので、連想リストの先頭にキーと値を追加すれば、以前の値を隠蔽して直近に登録した値を求めることができます。メソッド mem, find, find_opt は連想リストの探索関数を呼び出すだけです。find_all は高階関数 filter を使えば簡単です。

メソッド remove は連想リストの削除関数 remove_assoc を呼び出します。この関数もキーを線形探索するので、最初に見つけたキーと値を削除します。remove_all は filter を使ってすべてのキー k と値を削除します。あとは特に難しいところはないと思います。

●実行例

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

# #use "dic.ml";;
class ['k, 'v] dic :
  object
    val mutable xs : ('k * 'v) list
    method add : 'k -> 'v -> unit
    method clear : unit
    method find : 'k -> 'v
    method find_all : 'k -> ('k * 'v) list
    method find_opt : 'k -> 'v option
    method is_empty : bool
    method iter : ('k * 'v -> unit) -> unit
    method length : int
    method mem : 'k -> bool
    method remove : 'k -> unit
    method remove_all : 'k -> unit
  end
# let d = new dic;;
val d : ('_a, '_b) dic = <obj>
# d#is_empty;;
- : bool = true
# d#length;;
- : int = 0

# List.iter (fun (k, v) -> d#add k v) [("foo", 10); ("bar", 20); ("baz", 30)];;
- : unit = ()
# d#is_empty;;
- : bool = false
# d#length;;
- : int = 3
# d#mem "foo";;
- : bool = true
# d#find "bar";;
- : int = 20
# d#find_opt "baz";;
- : int option = Some 30
# d#find_opt "oops";;
- : int option = None

# d#add "foo" 100;;
- : unit = ()
# d#find_opt "foo";;
- : int option = Some 100
# d#remove "foo";;
- : unit = ()
# d#find_opt "foo";;
- : int option = Some 10

# d#add "bar" 200;;
- : unit = ()
# d#find_opt "bar";;
- : int option = Some 200
# d#remove_all "bar";;
- : unit = ()
# d#find_opt "bar";;
- : int option = None

# d#clear;;
- : unit = ()
# d#is_empty;;
- : bool = true
# d#length;;
- : int = 0

正常に動作していますね。今回は辞書に同じキーを複数登録できるようにしましたが、同じキーがあれば新しい値に更新する方法もあります。興味のある方はプログラムを改造してみてください。


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

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

[ PrevPage | OCaml | NextPage ]