プログラミングに興味のある方ならば、「オブジェクト指向」という言葉は聞いたことがあると思います。よく使われているオブジェクト指向言語に 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 のオブジェクト指向を説明するのは無謀なことだと思いますが、簡単なところから少しずつ勉強していきましょう。まず最初に、一般的なオブジェクト指向について簡単に説明します。
なお、この説明は拙作のページ「お気楽 Python プログラミング入門第 5 回」と同じです。既に読んだことがある方や、一般的なオブジェクト指向について理解されている方は、読み飛ばしてもらってもかまいません。
プログラムを作る場合、全体を小さな処理に分割して、ひとつひとつの処理を作成し、それらを組み合わせて全体のプログラムを完成させます。このとき、基本的な部品となるのが関数です。つまり、処理を関数単位で分割して、それらを組み合わせてプログラムを作るわけです。もともと関数の役割は、入力されたデータを処理してその結果を返すことです。つまり、関数は機能を表しているのです。このため、全体を小さな処理に分割するにしても、機能単位で行われることが普通です。
オブジェクト指向プログラミングでは、関数ではなく「オブジェクト (object)」を部品として扱います。たとえば、えんぴつを考えてみましょう。えんぴつには、色、長さ、固さ、などいろいろな性質があります。そして、えんぴつを使って文字を書いたり、絵を描いたりすることができます。プログラムでは、このような性質をデータで表し、機能を関数で表すことになります。そしてオブジェクトとは、このデータと関数を結び付けたものなのです。
いままでのプログラミング言語では、データと関数を別々に定義するため、それをひとつのオブジェクトとして表すことができません。えんぴつで文字を書くにも、えんぴつの種類をチェックして文字を書くようにプログラムしなければいけません。ところが、オブジェクトはデータと関数を結び付けたものなので、自分がなにをしたらよいかわかっています。えんぴつオブジェクトに文字を書けと命じれば、それが赤えんぴつのオブジェクトであれば文字は赤に、黒えんぴつのオブジェクトであれば黒い文字になるのです。
このように、オブジェクトはデータと関数をひとつにまとめたものです。従来のプログラミングが全体を機能単位で分割するのに対し、オブジェクト指向プログラミングでは全体をオブジェクト単位に分割して、それを組み合わせることでプログラムを作成します。
ところで、データと関数を結び付けることは、従来のプログラミング言語でも可能です。オブジェクト指向はプログラミングの考え方のひとつであり、C++ のようなオブジェクト指向言語を使わなくても、たとえばC言語でもその考え方にしたがってプログラムを作れば、オブジェクト指向プログラミングになります。
実際、オブジェクト指向には様々な考え方があり、いろいろなオブジェクト指向言語が存在します。ですが、データと関数をひとつにまとめたものをオブジェクトとして扱うという基本的な考え方は、オブジェクト指向言語の元祖と言われる Smalltalk でも、C++, Java, OCaml でも同じです。
次は、一般的なオブジェクト指向機能について簡単に説明します。
「クラス (class)」はオブジェクトの振る舞いを定義したものです。ここでデータを格納するための変数や、それを操作する関数が定義されます。この変数をメンバ変数とかインスタンス変数といい、関数を「メソッド (method)」といいます。メソッドはあとで説明します。
クラスはオブジェクトの設計図にあたるもので、オブジェクトの「雛形」と呼ぶこともあります。クラスはオブジェクトの振る舞いを定義するだけで、アクセスできる実体はなにも生み出していない、ということに注意してください。
このクラスから実体として作り出されるのが「インスタンス (instance)」です。このインスタンスを「オブジェクト」と考えてください。インスタンスを生成する方法は、当然ですがプログラミング言語によって違います。たとえば C++ や Java は new を使います。図 1 を見てください。
┌─ class Foo ─┐ ┌─ instance ─┐ │ │ │ │ │ 設計図 │─ インスタンスの生成 →│ 実体 │ │ │ │ │ └────────┘ └───────┘ │ │ │ ┌─ instance ─┐ │ │ │ └───── インスタンスの生成 →│ 実体 │ │ │ └───────┘ 図 1 : クラスとインスタンスの関係
クラスはオブジェクトの定義を表すものですから、Foo というクラスはひとつしかありません。これに対し、インスタンスはクラスから生み出されるオブジェクトです。たとえば、クラス Foo に new を適用することで、いくつでもインスタンスを生み出すことができるのです。クラスは設計図であり、それに従って作られるオブジェクトがインスタンスと考えるとわかりやすいでしょう。
メソッドはオブジェクトと結びついた関数です。オブジェクト指向プログラミングでは、ほかの関数から直接オブジェクトを操作することはせず、メソッドを呼び出すことで行います。メソッドは、クラスが異なっていれば同じ名前のメソッドを定義することができます。たとえば、クラス Foo1 にメソッド bar() が定義されていても、クラス Foo2 に同名のメソッド bar() を定義することができます。
そして、ここからが重要なのですが、あるオブジェクトに対してメソッド bar() を呼び出した場合、それが Foo1 から作られたオブジェクトであれば、Foo1 で定義された bar() が実行され、Foo2 から作られたオブジェクトであれば、Foo2 で定義された bar() が実行されるのです。
このように、オブジェクトが属するクラスによって、実行されるメソッドが異なるのです。この機能を「ポリモーフィズム(polymorphism)」と呼びます。これにより、オブジェクトは自分が行うべき適切な処理を実行できるわけです。
クラス、インスタンス、メソッドの関係は図 2 のようになります。
┌─ class Foo1 ─┐ ┌─ instance ─┐ │ │ │ │ │ 設計図 │─── 生成 ───→│ 実体 │ │ │ │ │ │ │ └───────┘ │┌─ method ─┐│ ↑ ││ ││ │ ││ bar()←─┼┼─── アクセス ─────┘ ││ ││ │└──────┘│ └────────┘ 図 2 : クラス、インスタンス、メソッドの関係
クラスという設計図が中心にあり、そこからインスタンスが生み出され、メソッドを使ってインスタンスを操作する、という関係になります。
さて、一般的な話はここまでにして、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 = ()
このように、型変数を渡すことで多相クラスを定義することができます。
もう一つ簡単な例としてキューを作ってみましょう。拙作のページ「モジュール」で作成した「キュー」をクラスを使って実装します。次のリストを見てください。
リスト 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 におけるクラスはデータ型そのものを表しているのではなく、オブジェクトのデータ型に「名前」を付けただけなのです。もちろん、名前をつけることには意味があり、オブジェクトの型をそのまま使うよりも、クラス名を使うことでわかりやすいプログラムを作ることができます。
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 は静的に強く型付けされる言語ですが、構造的部分型によりダック・タイピングのようなプログラミングスタイルも可能になっています。もちろん、コンパイル時に静的な型チェックが行われるので、エラーを検出することもできます。
最後に、クラスの簡単な例題として、モジュール 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
正常に動作していますね。今回は辞書に同じキーを複数登録できるようにしましたが、同じキーがあれば新しい値に更新する方法もあります。興味のある方はプログラムを改造してみてください。