10.1 Structure文によるモジュールの定義と利用
問 10.1
第8.3節で定義した循環2重リストを使い, IntQueue と置き換え可能なストラクチャ ImperativeIntQueue を定義し, そのテストを行え.
解答例 この例を実装するために、まず、循環2重リストのコードを 以下のDListストラクチャにまとめる。
structure DList = struct datatype ’a cell = NIL | CELL of {data:’a, left:’a cell ref, right:’a cell ref} exception EMPTY_DLIST type ’a dlist = ’a cell ref fun emptyDlist () = ref NIL fun rightDlist (ref (CELL{right,...})) = right | rightDlist _ = raise EMPTY_DLIST fun leftDlist (ref (CELL{left,...})) = left | leftDlist _ = raise EMPTY_DLIST fun dataDlist (ref (CELL{data,...})) = data | dataDlist _ = raise EMPTY_DLIST fun singleton a = let val l = ref NIL val r = ref NIL val c = CELL{left=l, right=r, data=a} in (l:=c; r:=c; ref c) end fun member x nil = false | member x (h::t) = x = h orelse member x t fun insert a dlist = case dlist of ref (CELL{left=l1 as ref (CELL{right=r1,...}),...}) => let val newcell = CELL{data=a, right=ref (!dlist), left=ref (!l1)} in (dlist:=newcell; l1:=newcell; r1:=newcell) end | ref NIL => let val l = ref NIL val r = ref NIL val cell = CELL{data=a,left=l,right=r} in (dlist:=cell; l:=cell; r:=cell) end | _ => raise EMPTY_DLIST fun deleteDlist dlist = case dlist of ref NIL => () | ref (CELL{left=l1 as ref (CELL{right=r2,left=l2,...}), right=r1 as ref (CELL{right=r3,left=l3,...}), ...}) => if l1 = l2 then dlist := NIL else (dlist := !r1; r2 := !r1; l3 := !l1) fun toList L = let fun f l visited = if member l visited then nil else (dataDlist l)::(f (rightDlist l) (l::visited)) in f (rightDlist (leftDlist L)) nil end fun fromList (L:int list) = foldl (fn (x,y) => (insert x y;y)) (ref NIL) L fun concatDlist D1 D2 = case (D1, D2) of (ref NIL, _) => D1 := !D2 | (_, ref NIL) => D2 := !D1 | (ref (CELL{left=d1l as ref (CELL{right=d1lr,...}),...}), ref (CELL{left=d2l as ref (CELL{right=d2lr,...}),...})) => let val d1lCell = !d1l val d1lrCell = !d1lr in (d1l := !d2l; d1lr := !d2lr; d2l := d1lCell; d2lr := d1lrCell) end | _ => raise EMPTY_DLIST fun mapDlist f d = let fun newElem x nil = NONE | newElem x ((h,newH)::t) = if x = h then SOME newH else newElem x t fun copy l copied = case l of ref NIL => ref NIL | ref (CELL{left, right, data}) => (case newElem l copied of NONE => let val newL = ref NIL val copied = (l, newL)::copied val l = copy left copied val r = copy right copied in (newL := CELL{left = l, right = r, data = f data}; newL) end | SOME newL => newL ) in copy d nil end fun copyDlist d = mapDlist (fn x => x) d fun foldrDlist F z d = let fun f d z visited = if member d visited then z else F (dataDlist d, f (rightDlist d) z (d::visited)) in f (rightDlist (leftDlist d)) z nil end fun foldlDlist F z d = let fun f d z visited = if member d visited then z else f (rightDlist d) (F (dataDlist d, z)) (d::visited) in f (rightDlist (leftDlist d)) z nil end end
ImperativeIntQueueストラクチャは、このDListストラクチャを 使って、以下のように定義される。
structure ImperativeIntQueue = struct exception EmptyQueue type queue = int DList.dlist fun newQueue() = DList.emptyDlist() : queue fun enqueue (item,queue) = DList.insert item queue fun dequeue queue = let val last = DList.leftDlist queue val data = DList.dataDlist last in (DList.deleteDlist last; data) end handle DList.EMPTY_DLIST => raise EmptyQueue end
以下は、実行結果である。
# val q = ImperativeIntQueue.newQueue(); val q = ref NIL : int DList.cell ref # val _ = ImperativeIntQueue.enqueue(1,q); # val x = ImperativeIntQueue.dequeue q; val x = 1 : int
問 10.2
FastIntQueue が正しく待ち行列を実現していることを確認せよ. すなわち,空の待ち行列から始めて enqueue と dequeue 操 作を繰り返したときの動作が,単純なリストによる場合と同一であることを示せ.
解答例 空の待ち行列から始めてenqueue(Q, )を回、 dequeueを実施した場合を考える。 単純なリストの場合、リストの中身は、 で あり、次のdequeue操作で、が返される。 任意のについて、FastQueueの場合も、この状 態と同等の状態が保たれることが示せればよい。 FastIntQueueの操作途中の状態は、2つのリストの組である。この2つのリストの中の要素を
とする。 すると、
が成立し、次に返される値は、であり、 リストを用いた待ち行列と同一である。 この性質が、任意ので成り立つから、 FastQueueは単純なリストを用いた実装と同一の振る舞いをする。
問 10.3
enqueue と dequeue がランダムに行われる場合の,dequeue の平均の実行時間を見積もれ.
解答例 dequeueに掛かる実行時間を
-
1.
リストに要素を1つ追加するための必要な時間
-
2.
リストから先頭要素を1つ取り出すのに必要な時間
の回数で見積もる。 が空でない合はである。 が空の場合、の長さをとすると、 に等しく、 これら回数の平均を見積もることである。
この問題は、amortize cost(償却原価)の考え方を使えば、統計的・解析的な計算をせず、 即座に求めることができる。 dequeueで取り除かれる要素に着目する。 この要素は、enqueueでのリストの先頭に追加され、 さらに、dequeue操作でとりだされる前のいずれかのdequeue操作で のリストから取り除かれ、のリストの先頭に追加される。 このコストはである。 そこで、このが待ち行列に追加された時、この要素を dequeueするための将来必要なコスト原価として計上しておく、と考える。 すると、dequeue操作では、が空の時に、から へ移動させるコストはすでにこの原価に含まれている、と考え、 取り除かれる要素のコストは、を取り除くための原価() +実際に掛かるコスト()と計算できる。 以上から、dequeue操作の平均時間はである。
ちなみに、enqueue操作の平均時間は、であるから、 FastQueueの要素あたりのenqueue、dequeueコストは であることがわかる。
問 10.4
IntQueue をFastIntQueue に置き換えてテストを行え.
解答例 教科書にあるコードの通りIntQueueが定義されている環境で、たとえば、 以下のようなコードが可能である。
structure Q = IntQueue val q = Q.newQueue() val _ = Q.enqueue(1, q) val _ = Q.enqueue(2, q) val _ = Q.enqueue(3, q) val a = Q.dequeue q val b = Q.dequeue q val c = Q.dequeue q
SML#の対話型環境では、以下のような実行結果を得る。
# structure Q = IntQueue; structure Q = struct type queue = int list ref exception EmptyQueue = IntQueue.EmptyQueue val newQueue = fn : unit -> int list ref val enqueue = fn : [’a. ’a * ’a list ref -> unit] val removeLast = fn : [’a. ’a list -> ’a list * ’a] val dequeue = fn : [’a. ’a list ref -> ’a] end # val q = Q.newQueue(); val q = ref [] : int list ref # val _ = Q.enqueue(1, q); # val _ = Q.enqueue(2, q); # val _ = Q.enqueue(3, q); # val a = Q.dequeue q; val a = 1 : int # val b = Q.dequeue q; val b = 2 : int # val c = Q.dequeue q; val c = 3 : int #
このQストラクチャの定義のみを、FastIntQueueに置き換えると以下 のコードを得る。
structure Q = FastIntQueue val q = Q.newQueue() val _ = Q.enqueue(1, q) val _ = Q.enqueue(2, q) val _ = Q.enqueue(3, q) val a = Q.dequeue q val b = Q.dequeue q val c = Q.dequeue q
SML#の対話型環境では、以下のような実行結果を得る。
# structure Q = FastQueue; structure Q = struct type elem = int32 type queue = elem list ref * elem list ref exception EmptyQueue = FastQueue.EmptyQueue val newQueue = fn : unit -> elem list ref * elem list ref val enqueue = fn : [’a, ’b. ’a * (’a list ref * ’b) -> unit] val dequeue = fn : [’a. ’a list ref * ’a list ref -> ’a] end # val q = Q.newQueue(); val q = (ref [], ref []) : int list ref * int list ref # val _ = Q.enqueue(1, q); # val _ = Q.enqueue(2, q); # val _ = Q.enqueue(3, q); # val a = Q.dequeue q; val a = 1 : int # val b = Q.dequeue q; val b = 2 : int # val c = Q.dequeue q; val c = 3 : int
この結果から、同一の動作をしており、置き換え可能であることが確認できる。