M.Hiroi's Home Page

Functional Programming

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

[ PrevPage | OCaml | NextPage ]

継承

前回まででオブジェクト指向の基本であるクラス、インスタンス、メソッドについて一通り説明しました。今回はオブジェクト指向機能の目玉ともいえる「継承」について説明します。

OCaml の継承は他のオブジェクト指向言語と比べると、基本的な考え方は同じですが、クラスの継承関係が必ずしも「部分型」にはならない場合があるので注意が必要です。まず最初に、一般的なオブジェクト指向言語で使われている継承について簡単に説明します。

なお、このドキュメントは拙作のページ Lightweight Language お気楽 Python プログラミング入門第 6 回 と同じです。既に読んだことがある方や、継承について理解されている方は読み飛ばしてもらってもかまいません。

次へ

●継承とは?

「継承 (inheritance : インヘリタンス)」は、簡単に言うとクラスに「親子関係」を持たせる機能です。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたインスタンス変数やメソッドになります。

プログラムを作る場合、いままで作ったプログラムと同じような機能が必要になることがありますが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる「差分プログラミング」が可能になります。

クラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++ の場合は、元になるクラスを基本クラスといい、継承するクラスを派生クラスとか導出クラスといいます。

たとえば、クラス Foo1 を継承してクラス Foo2 を定義しましょう。クラス Foo1 にはメソッド bar が定義されています。クラス Foo2 にメソッド bar は定義されていませんが、Foo2 のオブジェクトに対して bar を呼び出すと、スーパークラス Foo1 のメソッド bar が実行されるのです。

メソッドの選択は次のように行われます。まず、オブジェクトが属するクラス Foo2 にメソッド bar が定義されているか調べます。ところが、Foo2 には bar が定義されていないので、スーパークラスである Foo1 に bar が定義されているか調べます。ここでメソッド bar が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合はエラーとなります。

継承したクラスのメソッドとは違う働きをさせたい場合、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。これを「オーバーライド (over ride)」といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって探索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるわけです。

●単一継承と多重継承

継承には「単一継承」と「多重継承」の 2 種類があります。単一継承は、ただひとつのクラスからしか機能を継承することができません。したがって、クラスの階層は図 1 のような木構造で表すことができます。


  図 1 : 単一継承におけるクラスの階層

継承は何段階に渡って行われてもかまいません。たとえばクラス E の場合、スーパークラスが B で、B のスーパークラスが A に設定されています。サブクラスは複数あってもかまいません。たとえば、A のサブクラスは B, C, D と 3 つ、B のサブクラスは E, F と 2 つあります。図 1 の場合、クラス A のスーパークラスはありませんが、ほかのクラスではただひとつのスーパークラスを持っています。プログラミング言語では、Java や Smalltalk が単一継承です。

これに対し多重継承は、複数のクラスを継承することができます。このため、クラスの階層は木構造ではなく、図 2 のようなグラフ [*1] で表すことができます。


  図 2 : 多重継承におけるクラスの階層

クラス E に注目してください。スーパークラスには B と C の 2 つがあります。多重継承では、単一継承と同じくサブクラスを複数持つことができ、なおかつ、スーパークラスも複数持つことができるのです。プログラミング言語では、C++ や Common Lisp Object System (CLOS) が多重継承をサポートしています。そして、OCaml も多重継承です。

実をいうと、M.Hiroi は多重継承に対してあまりいいイメージを持っていません。私見ですが、多重継承はメリットよりもプログラムを複雑にするデメリットの方が大きいのではないか、と思っています。特に、図 2 のクラス A, B, C, E のような菱形の関係を C++ でプログラムする場合、とても複雑な問題を引き起こすことが知られています。OCaml の場合でも、多重継承で問題が発生することがあります。多重継承については次回で詳しく説明します。

-- note --------
[*1] グラフは木をより一般化したデータ構造です。数学のグラフ理論では、いくつかの点とそれを結ぶ線でできた図形を「グラフ」といいます。

●継承の仕組み

一般的なオブジェクト指向言語の場合、継承によって引き継がれる性質は定義されたインスタンス変数やメソッドになります。図 3 を見てください。


  図 3 : 一般的なオブジェクト指向言語における継承

クラス Foo にはインスタンス変数 a, b とメソッド get_a, get_b が定義されています。次に、クラス Bar を定義します。Bar は Foo を継承し、Bar 固有のインスタンス変数 c とメソッド get_c を定義します。Foo と Bar のインスタンスを生成すると、図 3 に示したように、Bar のインスタンスにはクラス Foo で定義された変数 a, b も含まれます。このように、Foo のインスタンス変数が Bar に継承されます。

Foo のインスタンスを生成すると、もちろん変数 a, b は含まれていますが、Bar のインスタンスとメモリを共有することはありません。クラスはオブジェクトの設計図です。設計に共通な部分があったとしても、それから生み出されるインスタンスは別々の実体で、インスタンス変数を共有することはないのです。

クラス Bar にはメソッド get_c しか定義されていませんが、クラス Foo を継承することにより、メソッド get_a と get_b を利用することができます。Bar のインスタンスに対して get_a を呼び出すと、クラス Bar には get_a が定義されていないので、スーパークラス Foo を調べ、そこで定義されている get_a が呼び出されます。もちろん、取り出される値は Bar のインスタンスにある変数 a の値です。このように、Foo のメソッドが Bar に継承されます。

●単一継承の使い方

それでは、具体的に OCaml の継承を説明しましょう。スーパークラスは object ... end の中で inherit 宣言を使って指定します。また、inherit 宣言を複数並べると、多重継承を行うことができます。継承に必要な設定はこれだけです。

簡単な例として、図 3 のクラスを実際にプログラムしてみましょう。まずクラス foo を定義します (リスト 1)。

リスト 1 : クラス foo の定義

class ['a] foo x y =
  object
    val a = (x: 'a)
    val b = (y: 'a)
    method get_a = a
    method get_b = b
  end

インスタンス変数 a, b は引数 x, y で初期化します。データ型は型変数 'a で表します。メソッド get_a と get_b の定義は簡単です。与えられたインスタンスから値を取り出すだけです。次にクラス bar を定義します (リスト 2)。

リスト 2 : クラス Bar の定義

class ['a] bar x y z =
  object
    inherit ['a] foo x y
    val c = (z: 'a)
    method get_c = c
  end

inherit でスーパークラス foo を指定します。このとき、必要な型変数や引数を指定します。これで foo で定義されているインスタンス変数が初期化されます。あとは自分のクラスで使うインスタンス変数 c を初期化し、メソッド get_c を定義します。

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

class ['a] foo :
  'a ->
  'a -> object val a : 'a val b : 'a method get_a : 'a method get_b : 'a end

class ['a] bar :
  'a ->
  'a ->
  'a ->
  object
    val a : 'a
    val b : 'a
    val c : 'a
    method get_a : 'a
    method get_b : 'a
    method get_c : 'a
  end

クラス foo を継承したことにより、クラス bar には foo のインスタンス変数とメソッドが引き継がれていることが分かります。なお、クラス bar は foo にメソッド get_c を追加しているだけなので、bar は foo の部分型になります。

それでは実行してみましょう。

# let a = new foo 1 2;;
val a : int foo = <obj>
# let b = new bar 10 20 30;;
val b : int bar = <obj>
# a#get_a;;
- : int = 1
# a#get_b;;
- : int = 2
# b#get_a;;
- : int = 10
# b#get_b;;
- : int = 20
# b#get_c;;
- : int = 30

メソッド get_a は bar に定義されていませんが、スーパークラス foo のメソッド get_a が呼び出されて、インスタンス変数 a の値を求めることができます。また、bar のインスタンスに対して get_c を呼び出せば、インスタンス変数 c の値を求めることができます。

●オーバーライド

継承はクラスに新しい機能を追加するだけではなく、メソッドをオーバーライドすることで機能を変更することができます。簡単な例題として、前々回作成したスタックを継承して、格納する要素数を制限するスタック fixed_stack というクラスを作ってみましょう。リスト 3 を見てください。

リスト 3 : 制限付きスタック

exception Full

class ['a] fixed_stack (limit: int) =
  object
    inherit ['a] stack as super
    val mutable size = 0

    method push x =
      if size = limit then raise Full
      else size <- size + 1; super#push x

    method pop =
      if size = 0 then raise Empty
      else size <- size - 1; super#pop

    method is_full = size = limit
  end

fixed_stack は指定した上限値までしか要素を格納できません。stack で要素を追加するメソッドは push で、削除するメソッドは pop です。この 2 つのメソッドをオーバーライドすることで、fixed_stack の機能を実現することができます。スタックの上限値は引数 limit で指定し、スタックの要素数はインスタンス変数 size で管理します。

fixed_stack は stack を継承するので、inherit でスーパークラスに stack を指定します。メソッドをオーバーライドするとき、スーパークラスのメソッドを呼び出すことができると便利です。これを「メソッド結合 (method combination)」といいます。OCaml の場合、inherit でスーパークラスを指定しますが、このとき次のように名前を指定すると、その名前を使ってスーパークラスのメソッドを呼び出すことができます。

inherit スーパークラス名 as 名前

fixed_stack では、名前に super を指定しているので、super#push や super#pop でスーパークラス stack のメソッドを呼び出すことができます。

メソッド push は limit と size を比較して、size が limit と等しい場合はデータを挿入できないので例外 Full を送出します。そうでない場合はスーパークラスのメソッド push を呼び出して、データを挿入して size を +1 します。

メソッド pop の場合、size が 0 であればデータを削除できないので例外 Empty を送出します。size が 0 よりも大きいときにスーパークラスのメソッド pop を呼び出して、size を -1 します。これで、スタックに格納される要素数を管理することができます。

簡単な実行例を示しましょう。

# let a = new fixed_stack 5;
val a : '_a fixed_stack = <obj>
# for i = 0 to 4 do a#push i done;;
- : unit = ()
# a#push 5;;
exception: Full
# a#is_full;;
- : bool = true
# while not a#is_empty do print_int a#pop; print_string " " done;;
4 3 2 1 0 - : unit = ()

このように stack を継承することで、fixed_stack を簡単にプログラムすることができます。

●集合 (2)

もう一つ簡単な例題として、前回作成した集合 set を継承して、要素を昇順に並べて格納する集合 sorted_set を作ってみましょう。sorted_set はリストは要素をまとめてソートするのではなく、要素が昇順に並ぶように適切な位置にデータを挿入します。また、要素を昇順に並べておくと、最小値を簡単に求めることができます。

プログラムをリスト 4 に示します。

リスト 4 : 順序付き集合

exception Empty

class ['a] sorted_set compare =
  object
    inherit ['a] set compare

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

    method insert p =
      let rec ins = function
        [] -> [p]
      | (x::xs) as ls -> let r = compare p x in
          if r = 0 then ls
          else if r < 0 then p::ls
          else x :: (ins xs)
      in
        content <- ins content

    method min =
      match content with
        [] -> raise Empty
      | x::_ -> x

    method delete_min =
      match content with
        [] -> raise Empty
      | x::xs -> content <- xs; x
  end

sorted_set は set を継承するので、inherit で ['a] set と指定します。このとき、要素を比較する関数 compare も忘れずに指定してください。オーバーライドするメソッドは member と insert です。データを昇順に並べると、探索処理は線形探索よりも速くなります。メソッド min は最小値のデータを返します。メソッド delete_min は集合から最小値のデータを取り除きます。

member は簡単にプログラムできます。要素は昇順に並んでいるので、引数 p よりも要素 x が大きい場合、それ以降に p と等しい要素は存在しません。ここで探索を打ち切って false を返すことができます。

データを挿入する insert も簡単です。これは拙作のページ ソート で取り上げた「挿入ソート」と同じ考え方です。データ p と要素を比較していき、p よりも大きい要素の前に p を挿入します。リストが空リストになった場合、p が一番大きなデータなので [p] を返します。

メソッド min は content の先頭の要素を返すだけです。メソッド delete_min は content の先頭の要素を取り除き、その要素を返します。どちらのメソッドも content が空リストの場合は例外 Empty を送出します。

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

class ['a] sorted_set :
  ('a -> 'a -> int) ->
  object ('b)
    val mutable content : 'a list
    val mutable size : int
    method copy : 'b
    method delete : 'a -> unit
    method delete_min : 'a
    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 min : 'a
    method union : 'b -> 'b
  end

set から継承したバイナリメソッドの型に注目してください。引数の型が自分自身 (sorted_set) と同じ 'b になっていますね。もしも、クラス set でバイナリメソッドの引数の型を 'a set とすると、sorted_set のバイナリメソッドの引数の型も 'a set になってしまうので、sorted_set の集合演算を定義することができません。

また、集合演算を行うメソッドでは返り値が 'b になっています。もしも、オブジェクトをコピーしないで新しい集合を new set で生成すると、集合演算の返り値は 'a set になってしまいます。自分自身の型を指定し、オブジェクトをコピーすることで、set を継承した sorted_set を正しく定義することができます。

ただし、問題点がないわけではありません。実は、sorted_set は set の部分型にはなりません。このことについては次回で詳しく説明します。

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

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

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

●initializer

インスタンスを生成した後で独自の処理を行いたい場合は initializer を設定します。

initializer 式

initializer はインスタンスが生成されてから呼び出されます。このとき、インスタンス変数の値は初期化されています。式の返り値は unit でなければいけません。また、initializer の中で自分のクラスのメソッドを呼び出すこともできます。簡単な例を示しましょう。

リスト 5 : initializer

class foo (xi: int) =
  object
    val mutable x = xi
    initializer 
      print_int x; print_newline ()

    method get_x = x
    method set_x a = x <- a
  end

class bar (xi: int) (yi: int) =
  object
    inherit foo xi
    val mutable y = yi
    initializer 
      print_int y; print_newline ()

    method get_y = y
    method set_y a = y <- a
  end

クラス foo と bar の initializer はインスタンス変数の値を表示します。initializer はオーバーライドされることはなく、継承したクラスの initializer は設定されていれば必ず実行されます。

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

# let a = new foo 10;;
10
val a : foo = <obj>
# let b = new bar 1 2;;
1
2
val b : bar = <obj>

bar のインスタンスを生成すると、foo の initializer が評価されて、その次に bar の initializer が評価されていることがわかります。

●抽象メソッドと抽象クラス

クラスでメソッドを定義するとき、キーワード virtual を付けるとメソッドの型だけを宣言することができます。これを「抽象メソッド (virtual method)」といいます。そして、抽象メソッドを持つクラスを「抽象クラス (virtual class)」といい、new でインスタンスを生成することはできません。抽象クラスと抽象メソッドは次のように定義します。

class virtual クラス名 args ... =
  object
    method virtual メソッド名 : 型式
    ...
  end

抽象クラスは継承されることを前提としたクラスで、抽象メソッドはサブクラスにおいて具体的に定義されます。抽象クラスでは、サブクラス共通のメソッドを定義します。このとき、抽象クラスのメソッドは抽象メソッドを使って定義することができます。サブクラスのインスタンスが生成されるとき、そのサブクラスでは抽象メソッドが具体化されているはずなので、抽象クラスのメソッドからサブクラスのメソッドが呼び出されることになります。

それでは簡単な例題として、図形の面積を求めるプログラムを作ってみましょう。次のリストを見てください。

リスト 6 : 図形のクラス

let pi = 3.14159265

(* 抽象クラス *)
class virtual figure =
  object (self)
    method virtual kind_of : string
    method virtual area : float

    method print =
      Printf.printf "%s: area = %f\n" self#kind_of self#area
  end

クラス figure は抽象クラスです。メソッド kind_of と area が抽象メソッドで、kind_of は図形の種類を文字列で返し、area は図形の面積を計算して返します。print は図形の種別と面積を表示するメソッドです。ここで、抽象メソッド kind_of と area を呼び出しています。kind_of と area はサブクラスで定義します。

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

class virtual figure :
  object
    method virtual area : float
    method virtual kind_of : string
    method print : unit
  end

次は図形を表すサブクラスを定義します。

リスト 7 : サブクラスの定義

(* 三角形 *)
class triangle altitude base =
  object
    inherit figure
    method get_altitude = altitude
    method get_base = base
    method kind_of = "Triangle"
    method area = altitude *. base /. 2.0
  end

(* 四角形 *)
class rectangle width height =
  object
    inherit figure
    method get_width = width
    method get_height = height
    method kind_of = "Rectangle"
    method area = width *. height
  end

(* 円 *)
class circle radius = 
  object
    inherit figure
    method get_radius = radius
    method kind_of = "Circle"
    method area = radius *. radius *. pi
  end

triangle, rectangle, circle は figure を継承します。サブクラス固有のメソッドも定義されていますが、どのクラスも抽象メソッド kind_of と area を具体化しています。なお、スーパークラスの抽象メソッドをすべて具体化しないと、そのサブクラスも抽象クラスになるため、インスタンスを生成することができません。ご注意ください。

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

class triangle :
  float ->
  float ->
  object
    method area : float
    method get_altitude : float
    method get_base : float
    method kind_of : string
    method print : unit
  end

class rectangle :
  float ->
  float ->
  object
    method area : float
    method get_height : float
    method get_width : float
    method kind_of : string
    method print : unit
  end

class circle :
  float ->
  object
    method area : float
    method get_radius : float
    method kind_of : string
    method print : unit
  end

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

# let a = new triangle 2.0 2.0;;
val a : triangle = <obj>
# let b = new rectangle 2.0 2.0;;
val b : rectangle = <obj>
# let c = new circle 2.0;;
val c : circle = <obj>
# a#print;;
Triangle: area = 2.000000
- : unit = ()
# b#print;;
Rectangle: area = 4.000000
- : unit = ()
# c#print;;
Circle: area = 12.566371

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

●型変換

OCaml では、型 A の部分型のオブジェクトは型 A のオブジェクトに変換することができます。この機能を「型変換 (coercion)」といいます。これは Java などのオブジェクト指向言語の「アップキャスト」に相当します。なお、OCaml のオブジェクト指向には「ダウンキャスト」に相当する機能はありません。型変換の構文を示します。

(式 : 型1 :> 型2)

式の評価値 (オブジェクト) の型 1 を型 2 に変換します。なお、たいていの場合は ": 型1" の部分を省略することができます。

たとえば、図形のオブジェクトをリストにまとめて格納することを考えます。triangle, rectangle, circle は型が違うので、同じリストに格納することはできません。この場合、figure に型変換すると同じリストに格納することができます。次の例を見てください。

# let sum_of_figure ls = List.fold_left (fun x y -> x +. y#area) 0.0 ls;;
val sum_of_figure : < area : float; .. > list -> float = <fun>
# sum_of_figure [(a :> figure); (b :> figure); (c :> figure)];;
- : float = 18.5663706

関数 sum_of_figure は図形の面積の合計値を求めます。型変換した場合、サブクラスの情報は失われるため、サブクラス独自のメソッドを呼び出すことはできません。型変換したスーパークラスのメソッドしか利用できませんが、ポリモーフィズムによりサブクラスのメソッドが呼び出されるため、図形の面積を正しく計算することができます。

●問題

可変長の配列を表すクラス arraylist を定義してください。













●解答

リスト : 可変長配列クラス arraylist

exception Empty
exception Out_of_range

class ['a] arraylist init_size (init_value : 'a) =
object
  val mutable buff = Array.make init_size init_value
  val mutable top = 0

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

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

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

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

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

  method length = top
end
exception Empty
exception Out_of_range
class ['a] arraylist :
  int ->
  'a ->
  object
    val mutable buff : 'a array
    val mutable top : int
    method get : int -> 'a
    method length : int
    method peek : 'a
    method pop : 'a
    method push : 'a -> unit
    method set : int -> 'a -> unit
  end

インスタンス変数 buff に配列本体を、インスタンス変数 top はスタックトップの位置を表します。これは配列の要素数と同じで、0 に初期化します。メソッド push は x を追加するとき、buff の大きさと top を比較します。同じ値であれば満杯なので x を追加することはできません。大きさが 2 倍の配列を生成して変数 newbuff にセットします。それから、buff の要素を newbuff にコピーします。これは Array の関数 blit を使うと簡単です。

# Array.blit;;
- : 'a array -> int -> 'a array -> int -> int -> unit = <fun>

blit a1 s1 a2 s2 size は配列 a1 の s1 番目から size 個の要素を配列 a2 の s2 番目以降にコピーします。簡単な使用例を示します。

# let a = [|1; 2; 3; 4; 5|];;
val a : int array = [|1; 2; 3; 4; 5|]
# let b = Array.make 10 0;;
val b : int array = [|0; 0; 0; 0; 0; 0; 0; 0; 0; 0|]
# Array.blit a 0 b 0 5;;
- : unit = ()
# b;;
- : int array = [|1; 2; 3; 4; 5; 0; 0; 0; 0; 0|]
# Array.blit a 0 b 5 5;;
- : unit = ()
# b;;
- : int array = [|1; 2; 3; 4; 5; 1; 2; 3; 4; 5|]

あとは newbuff を buff にセットし、top の位置に x を書き込んでから top の値を +1 します。

メソッド pop は top の値を -1 してから、buff.(top) の値を返します。メソッド peek は buff.(top - 1) の値を返します。top が 0 の場合、配列は空なので例外 Empty を送出します。メソッド get, set は簡単ですね。添字 i が範囲外であれば例外 Out_of_range を送出します。メソッド length は top の値を返すだけです。

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

# let a = new arraylist 4 0;;
val a : int arraylist = <obj>
# a#length;;
- : int = 0
# for i = 1 to 8 do a#push i done;;
- : unit = ()
# a#length;;
- : int = 8
# for i = 0 to 7 do print_int (a#get i); print_newline() done;;
1
2
3
4
5
6
7
8
- : unit = ()
# for i = 0 to 7 do a#set i (10 * a#get i) done;;
- : unit = ()
# for i = 0 to 7 do print_int (a#get i); print_newline() done;;
10
20
30
40
50
60
70
80
- : unit = ()
# for i = 1 to 8 do print_int (a#pop); print_newline() done;;
80
70
60
50
40
30
20
10
- : unit = ()
# a#length;;
- : int = 0
# a#get 0;;
Exception: Out_of_range.
# a#set 0 100;;
Exception: Out_of_range.
# a#pop;;
Exception: Empty.

どうやら正常に動作しているようです。興味のある方はいろいろ試してみてください。


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

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

[ PrevPage | OCaml | NextPage ]