Cs51 abstract data types ocaml modules
This presentation is the property of its rightful owner.
Sponsored Links
1 / 132

CS51: Abstract Data Types & OCaml Modules PowerPoint PPT Presentation


  • 86 Views
  • Uploaded on
  • Presentation posted in: General

CS51: Abstract Data Types & OCaml Modules. Greg Morrisett. The reality of development. We rarely know the right algorithms or the right data structures when we start a design project.

Download Presentation

CS51: Abstract Data Types & OCaml Modules

An Image/Link below is provided (as is) to download presentation

Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.


- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -

Presentation Transcript


Cs51 abstract data types ocaml modules

CS51: Abstract Data Types & OCamlModules

Greg Morrisett


The reality of development

The reality of development

  • We rarely know the right algorithms or the right data structureswhen we start a design project.

    • For Moogle, what data structures and algorithms should you use to build the index? To build the query evaluator?

  • Reality is that we often have to go back and change our code, once we’ve built a prototype.

    • Often, we don’t even know what the user wants (requirements) until they see a prototype.

    • Often, we don’t know where the performance problems are until we can run the software on realistic test cases.


Engineering for change

Engineering for change

We know the software will change, do how can we write the code so that the changes will be easier?


Engineering for change1

Engineering for change

  • We know the software will change, do how can we write the code so that the changes will be easier?

  • One trick: use a high-level language for the prototype.

    • languages like Python, Perl, Ruby, Ocaml, and Haskell make it relatively easy to throw together a prototype.

    • rule of thumb: 1 line of these modern high-level languages is worth about 10 lines of C/C++.

      • many details, like memory management, are handled automatically.

    • you may get lucky: the performance might be good enough that you never have to drop down to a lower-level.

    • more importantly, design exploration is easier on small pieces of code.


Engineering for change2

Engineering for change

  • We know the software will change, do how can we write the code so that the changes will be easier?

  • Another trick: steal code.

    • don’t develop your own libraries if you don’t have to.

    • e.g., string matching library.


Engineering for change3

Engineering for change

We know the software will change, do how can we write the code so that the changes will be easier?

Yet another trick: use data and algorithm abstraction.


Engineering for change4

Engineering for change

  • We know the software will change, do how can we write the code so that the changes will be easier?

  • Yet another trick: use data and algorithm abstraction.

    • Don’t code in terms of built-in concrete representations.

    • Design and code with high-level abstractions in mind that fit the problem domain.

    • Implement the abstractions using a well-defined interface.

    • This way, you can swap in different implementations for the abstractions.

    • You can also parallelize the development process.


Example

Example

For Moogle, one goal is to build a scalable dictionary (a.k.a. index) that maps words to the set of URLs for the pages on which the words appear. We want the index so that we can efficiently satisfy queries (e.g., all links to pages that contain “Greg” and “Victor”.)

Wrong way to think about this:

  • Aha! A list of pairs of a word and a list of URLs.

  • We can look up “Greg” and “Victor” in the list to get back a list of URLs.


Moogle example

Moogle example

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query) (h: (string * url list) list) : url list =

match qwith

| Word x ->

let (_,urls) = List.find (fun (w,_) -> w = x)

in urls

| And (q1,q2) -> merge_lists (eval q1 h) (eval q2 h)

| Or (q1,q2) -> (eval q1 h) @ (eval q2 h)


Moogle example1

Moogle example

merge expects to be passed sorted lists.

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query) (h: (string * url list) list) : url list =

match q with

| Word x ->

let (_,urls) = List.find (fun (w,_) -> w = x)

in urls

| And (q1,q2) -> merge_lists (eval q1 h) (eval q2 h)

| Or (q1,q2) -> (eval q1 h) @ (eval q2 h)


Moogle example2

Moogle example

merge expects to be passed sorted lists.

Oops!

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query) (h: (string * url list) list) : url list =

match q with

| Word x ->

let (_,urls) = List.find (fun (w,_) -> w = x)

in urls

| And (q1,q2) -> merge_lists (eval q1 h) (eval q2 h)

| Or (q1,q2) -> (eval q1 h) @ (eval q2 h)


Moogle example3

Moogleexample

I find out there’s a better hash-table implementation

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query)(h: (string, urllist) hashtable)

: url list =

match qwith

| Word x ->

let bucket= Array.get(hash_string x) h in

let (_,urls) = List.find x bucket

in urls

| And (q1,q2) -> merge_lists(eval q1 h) (eval q2 h)

| Or (q1,q2) -> (eval q1 h) @ (eval q2 h)


A better way

A better way

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query) (d: (string, url_set) dictionary)

: url_set =

match qwith

| Word x -> Dict.lookupdx

| And (q1,q2) -> Set.intersect (eval q1 d) (eval q2 d)

| Or (q1,q2) -> Set.union (eval q1 d) (eval q2 d)


A better way1

A better way

The problem domain talked about an abstract type of dictionaries and sets of URLs.

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query) (d: (string, url_set) dictionary)

: url_set =

match qwith

| Word x -> Dict.lookupdx

| And (q1,q2) -> Set.intersect (eval q1 d) (eval q2 d)

| Or (q1,q2) -> Set.union (eval q1 d) (eval q2 d)


A better way2

A better way

Once we’ve written the client, we know what operations we need on these abstract types.

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query) (d: (string, url_set) dictionary)

: url_set =

match q with

| Word x -> Dict.lookup d x

| And (q1,q2) -> Set.intersect (eval q1 d) (eval q2 d)

| Or (q1,q2) -> Set.union (eval q1 d) (eval q2 d)


A better way3

A better way

So we can define an interface, and send our partner off to implement the abstract types dictionary and set.

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query) (d: (string, url_set) dictionary)

: url_set =

match q with

| Word x -> Dict.lookup d x

| And (q1,q2) -> Set.intersect (eval q1 d) (eval q2 d)

| Or (q1,q2) -> Set.union (eval q1 d) (eval q2 d)


A better way4

A better way

Later on, when we find out linked lists aren’t so good for sets, we can replace them with balanced trees.

type query =

Word of string

| And of query * query

| Or of query * query

let rec eval (q: query) (d: (string, url_set) dictionary)

: url_set =

match q with

| Word x -> Dict.lookup d x

| And (q1,q2) -> Set.intersect (eval q1 d) (eval q2 d)

| Or (q1,q2) -> Set.union (eval q1 d) (eval q2 d)


Building abstract types in ocaml

Building abstract types in OCaml

  • We can use the module system of OCamlto build new abstract data types.

    • signature: an interface.

      • specifies the abstract type(s) without specifying their implementation

      • specifies the set of operations on the abstract types

    • structure: an implementation.

      • a collection of type and value definitions

      • notion of an implementation matching or satisfying an interface

        • gives rise to a notion of sub-typing

    • functor: a parameterized module

      • really, a function from modules to modules

      • allows us to factor out and re-use modules


Example signature

Example signature

module type INT_STACK =

sig

type stack

valempty : unit -> stack

valpush : int -> stack -> stack

valis_empty : stack -> bool

exceptionEmptyStack

valpop : stack -> stack

valtop : stack -> int

end


Example signature1

Example signature

empty and push are abstract constructors: functions that build our abstract type.

module type INT_STACK =

sig

type stack

valempty : unit -> stack

valpush : int -> stack -> stack

valis_empty : stack -> bool

exceptionEmptyStack

valpop : stack -> stack

valtop : stack -> int

end


Example signature2

Example signature

is_empty is an observer – useful for determining properties of the ADT.

module type INT_STACK =

sig

type stack

valempty : unit -> stack

valpush : int -> stack -> stack

valis_empty : stack -> bool

exceptionEmptyStack

valpop : stack -> stack

valtop : stack -> int

end


Example signature3

Example signature

pop is sometimes called a “mutator” (though it doesn’t really change the input)

module type INT_STACK =

sig

type stack

valempty : unit -> stack

valpush : int -> stack -> stack

valis_empty : stack -> bool

exceptionEmptyStack

valpop : stack -> stack

valtop : stack -> int

end


Example signature4

Example signature

top is also an observer, in this functional setting since it doesn’t change the stack.

module type INT_STACK =

sig

type stack

valempty : unit -> stack

valpush : int -> stack -> stack

valis_empty : stack -> bool

exceptionEmptyStack

valpop : stack -> stack

valtop : stack -> int

end


A better signature

A better signature

module type INT_STACK =

sig

type stack

(* create an empty stack *)

valempty : unit -> stack

(* push an element on the top of the stack *)

valpush : int -> stack -> stack

(* returns true iff the stack is empty *)

valis_empty : stack -> bool

(* raised on pop or top of empty stack *)

exceptionEmptyStack

(* pops top element and returns the rest *)

valpop : stack -> stack

(* returns the top element of the stack *)

valtop : stack -> int

end


Example structure

Example structure

module ListIntStack : INT_STACK =

struct

type stack = int list

exceptionEmptyStack

let empty () : stack = []

let push (i:int) (s:stack) = i::s

let is_empty (s:stack) =

match s with

| [] -> true

| _::_ -> false

let pop (s:stack) =

match s with

| [] -> raiseEmptyStack

| _::t -> t

let top (s:stack) =

match s with

| [] -> raiseEmptyStack

| h::_ -> h

end


Example structure1

Example structure

Inside the module, we know the concrete type used to implement the abstract type.

module ListIntStack : INT_STACK =

struct

type stack = int list

exceptionEmptyStack

let empty () : stack = []

let push (i:int) (s:stack) = i::s

let is_empty (s:stack) =

match s with

| [] -> true

| _::_ -> false

let pop (s:stack) =

match s with

| [] -> raiseEmptyStack

| _::t -> t

let top (s:stack) =

match s with

| [] -> raiseEmptyStack

| h::_ -> h

end


Example structure2

Example structure

But by giving the module the INT_STACK interface, which does not reveal how stacks are being represented, we prevent code outside the module from knowing stacks are lists.

module ListIntStack : INT_STACK =

struct

type stack = int list

exceptionEmptyStack

let empty () : stack = []

let push (i:int) (s:stack) = i::s

let is_empty (s:stack) =

match s with

| [] -> true

| _::_ -> false

let pop (s:stack) =

match s with

| [] -> raiseEmptyStack

| _::t -> t

let top (s:stack) =

match s with

| [] -> raiseEmptyStack

| h::_ -> h

end


An example c lient

An example client

Notice that the client is not allowed to know that the stack is a list.

module ListIntStack : INT_STACK =

struct

end

# let s= ListIntStack.empty ();;

# let s1 = ListIntStack.push 3 s;;

# let s2 = ListIntStack.push 4 s1;;

# ListIntStack.tops2 ;;

  • : int= 4

    # open ListIntStack ;;

    # top (pop s2) ;;

  • : int= 3

    # top (pop (pop s2)) ;;

    Exception: EmptyStack

    # List.revs2 ;;

    Error: This expression has type stack but an expression was expected of type ‘a list.


Example structure3

Example structure

Note that when you are debugging, you probably want to comment out the signature ascription so that you can access the contents of the module.

module ListIntStack(* : INT_STACK *) =

struct

type stack = int list

exceptionEmptyStack

let empty () : stack = []

let push (i:int) (s:stack) = i::s

let is_empty (s:stack) =

match s with

| [] -> true

| _::_ -> false

let pop (s:stack) =

match s with

| [] -> raiseEmptyStack

| _::t -> t

let top (s:stack) =

match s with

| [] -> raiseEmptyStack

| h::_ -> h

end


The client without the signature

The client without the signature

If we don’t seal the module with a signature, the client can know that stacks are lists.

module ListIntStack(* : INT_STACK *) =

struct

end

# let s= ListIntStack.empty ();;

# let s1 = ListIntStack.push 3 s;;

# let s2 = ListIntStack.push 4 s1;;

# ListIntStack.top s2 ;;

  • : int = 4

    # open ListIntStack ;;

    # top (pop s2) ;;

  • : int = 3

    # top (pop (pop s2)) ;;

    Exception: EmptyStack

    # List.rev s2 ;;

    - : int list = [3; 4]


Example structure4

Example structure

When you put the signature on here, you are restricting client access to the information in the signature (which does not reveal that stack = int list.)

So clients can only use the stack operations on a stack value (not list operations.)

module ListIntStack: INT_STACK =

struct

type stack = int list

exception EmptyStack

let empty () : stack = []

let push (i:int) (s:stack) = i::s

let is_empty (s:stack) =

match swith

| [] -> true

| _::_ -> false

let pop (s:stack) =

match swith

| [] -> raise EmptyStack

| _::t -> t

let top (s:stack) =

match swith

| [] -> raise EmptyStack

| h::_ -> h

end


A more familiar example

A more familiar example

moduletype UNSIGNED_BIGNUM =

sigtypeubignum

valfromInt : int -> ubignum

valtoInt : ubignum -> int

val plus : ubignum -> ubignum -> ubignum

val minus : ubignum -> ubignum -> ubignum

val times : ubignum -> ubignum -> ubignum

end


A s imple but slow implementation

A simple (but slow) implementation

module BIGNUM_UNARY : UNSIGNED_BIGNUM =

structtypeubignum = Zero | Succofubignum

let rec utoInt (u:ubignum) : int =

match u with

| Zero -> 0

| Succ u’ -> 1 + (toInt u’)

let rec plus (u1:ubignum) (u2:ubignum) : ubignum =

match u1 with

| Zero -> u2

| Succ u1’ -> uplus u1’ (Succ u2)

end


The abstraction barrier

The Abstraction Barrier

  • It is tempting to break through the abstraction barrier.

    • I know a bignum is either Zero or Succ

    • I know a stack is a list of elements

    • Why don’t we want to reveal this to clients?


A faster i mplementation

A faster implementation

module My_UBignum_1000 : UNSIGNED_BIGNUM =

struct

let base = 1000 typeubignum = int list

lettoInt(b:ubignum):int = …

let plus(b1:ubignum)(b2:ubignum):ubignum = …

letminus(b1:ubignum)(b2:ubignum):ubignum= …

let times(b1:ubignum)(b2:ubignum):ubignum= …

end


The abstraction barrier1

The Abstraction Barrier

  • It is tempting to break through the abstraction barrier.

    • I know a bignum is either Zero or Succ

    • I know a stack is a list of elements

    • Why don’t we want to reveal this to clients?

  • Two things:

    • might want to swap in a more sophisticated version

      • e.g., replace unary bignums with our more efficient list version

      • e.g., replace list-based stacks with growable arrays

      • e.g., replace association list with a balanced tree

      • A change would break clients

    • clients might break our internal representation invariants

      • e.g., might pass a list of coefficients with leading zeros

      • e.g., might pass in a zero bignum with the neg flag set

      • e.g., might pass in coefficients that are larger than the base

      • Forces us to code defensively


The abstraction barrier2

The Abstraction Barrier

Rule of thumb: try to use the language mechanisms (e.g., modules, interfaces, etc.) to enforce the abstraction barrier.

  • reveal as little information about how something is implemented as you can.

  • provides maximum flexibility for change moving forward.

  • pays off down the line

    However, like all design rules, we must be able to recognize when the barrier is causing more trouble than it’s worth and abandon it.

  • may want to reveal more information for debugging purposes

  • e.g., may forget to put a function in the interface to print out bignums.

  • (usually means you have a poor interface…)


Another example queues or fifos

Another example: Queues or FIFOs

module type QUEUE =

sig

type ‘a queue

valempty : unit -> ‘a queue

valenqueue : ‘a -> ‘a queue -> ‘a queue

valis_empty : ‘a queue -> bool

exception EmptyQueue

valdequeue : ‘a queue -> ‘a queue

valfront : ‘a queue -> ‘a

end


Another example queues or fifos1

Another example: Queues or FIFOs

These queues are re-usable for different element types.

module type QUEUE =

sig

type ‘a queue

valempty : unit -> ‘a queue

valenqueue : ‘a -> ‘a queue -> ‘a queue

valis_empty : ‘a queue -> bool

exception EmptyQueue

valdequeue : ‘a queue -> ‘a queue

valfront : ‘a queue -> ‘a

end


Another example queues or fifos2

Another example: Queues or FIFOs

So we parameterize the queue type by a type variable representing the element type (‘a).

module type QUEUE =

sig

type ‘a queue

valempty : unit -> ‘a queue

valenqueue : ‘a -> ‘a queue -> ‘a queue

valis_empty : ‘a queue -> bool

exception EmptyQueue

valdequeue : ‘a queue -> ‘a queue

valfront : ‘a queue -> ‘a

end


One implementation

One implementation

module AppendListQueue : QUEUE =

struct

type ‘a queue = ‘a list

let empty () = []

let enqueue (x:’a) (q:’a queue) : ‘a queue = q @ [x]

let is_empty (q:’a queue) =

match qwith

| [] -> true

| _::_ -> false

exception EmptyQueue

let deq (q:’a queue) : (‘a * ‘a queue) =

match qwith

| [] -> raise EmptyQueue

| h::t -> (h,t)

let dequeue (q:’a queue) : ‘a queue = snd (deq q)

let front (q:’a queue) : ‘a = fst (deq q)

end


One implementation1

One implementation

Notice deq is a helper function that doesn’t show up in the signature.

module AppendListQueue : QUEUE =

struct

type ‘a queue = ‘a list

let empty () = []

let enqueue (x:’a) (q:’a queue) : ‘a queue = q @ [x]

let is_empty (q:’a queue) =

match q with

| [] -> true

| _::_ -> false

exception EmptyQueue

let deq (q:’a queue) : (‘a * ‘a queue) =

match q with

| [] -> raise EmptyQueue

| h::t -> (h,t)

let dequeue (q:’a queue) : ‘a queue = snd (deq q)

let front (q:’a queue) : ‘a = fst (deq q)

end


One implementation2

One implementation

That means it is not accessible outside the module.

module AppendListQueue : QUEUE =

struct

type ‘a queue = ‘a list

let empty () = []

let enqueue (x:’a) (q:’a queue) : ‘a queue = q @ [x]

let is_empty (q:’a queue) =

match q with

| [] -> true

| _::_ -> false

exception EmptyQueue

let deq (q:’a queue) : (‘a * ‘a queue) =

match q with

| [] -> raise EmptyQueue

| h::t -> (h,t)

let dequeue (q:’a queue) : ‘a queue = snd (deq q)

let front (q:’a queue) : ‘a = fst (deq q)

end


One implementation3

One implementation

Notice also that deq runs in constant time.

module AppendListQueue : QUEUE =

struct

type ‘a queue = ‘a list

let empty () = []

let enqueue (x:’a) (q:’a queue) : ‘a queue = q @ [x]

let is_empty (q:’a queue) =

match q with

| [] -> true

| _::_ -> false

exception EmptyQueue

let deq (q:’a queue) : (‘a * ‘a queue) =

match q with

| [] -> raise EmptyQueue

| h::t -> (h,t)

let dequeue (q:’a queue) : ‘a queue = snd (deq q)

let front (q:’a queue) : ‘a = fst (deq q)

end


One implementation4

One implementation

Whereas append takes time proportional to the length of the queue

module AppendListQueue : QUEUE =

struct

type ‘a queue = ‘a list

let empty () = []

let enqueue (x:’a) (q:’a queue) : ‘a queue = q @ [x]

let is_empty (q:’a queue) =

match q with

| [] -> true

| _::_ -> false

exception EmptyQueue

let deq (q:’a queue) : (‘a * ‘a queue) =

match q with

| [] -> raise EmptyQueue

| h::t -> (h,t)

let dequeue (q:’a queue) : ‘a queue = snd (deq q)

let front (q:’a queue) : ‘a = fst (deq q)

end


An alternate implementation

An alternate implementation

module DoubleListQueue : QUEUE =

struct

type ‘a queue = {front: ’a list; rear: ’a list}

let empty () = {front = []; rear = []}

let enqueue x q = {front = q.front; rear= x::q.rear}

let is_emptyq =

match q.front, q.rearwith

| [], [] -> true

| _, _ -> false

exception EmptyQueue

let deq (q:’a queue) : ‘a * ‘a queue =

match q.frontwith

| h::t -> (h, {front=t; rear=q.rear})

| [] -> match List.revq.rearwith

| h::t -> (h, {front=t; rear=[]})

| [] -> raise EmptyQueue

let dequeue (q:’a queue) : ‘a queue = snd(deqq)

let front (q:’a queue) : ‘a = fst(deqq)

end


An alternate implementation1

An alternate implementation

The worst-case running time for deq is proportional to the length of the rear list.

module DoubleListQueue : QUEUE =

struct

type ‘a queue = {front:’a list; rear:’a list}

let empty() = {front=[]; rear=[]}

let enqueuexq = {front=q.front; rear=x::q.rear}

let is_emptyq =

match q.front, q.rearwith

| [], [] -> true

| _, _ -> false

exception EmptyQueue

let deq (q: ’a queue) : ‘a * ‘a queue =

match q.frontwith

| h::t -> (h, {front=t; rear=q.rear})

| [] -> match List.revq.rearwith

| h::t -> (h, {front=t; rear=[]})

| [] -> raise EmptyQueue

let dequeue (q: ’a queue) : ‘a queue = snd (deq q)

let front (q: ’a queue) : ‘a = fst (deqq1)

end


An alternate implementation2

An alternate implementation

But if we do a series of enqueues and dequeues, then each element will be flipped once from the rear to the front.

module DoubleListQueue : QUEUE =

struct

type ‘a queue = {front:’a list; rear:’a list}

let empty() = {front=[]; rear=[]}

let enqueuexq = {front=q.front; rear=x::q.rear}

let is_emptyq =

match q.front, q.rearwith

| [], [] -> true

| _, _ -> false

exception EmptyQueue

let deq (q:’a queue) : ‘a * ‘a queue =

match q.frontwith

| h::t -> (h, {front=t; rear=q.rear})

| [] -> match List.revq.rearwith

| h::t -> (h, {front=t; rear=[]})

| [] -> raise EmptyQueue

let dequeue (q:’a queue) : ‘a queue = snd(deqq)

let front (q:’a queue) : ‘a = fst(deqq)

end


An alternate implementation3

An alternate implementation

So the amortized cost of processing each element is constant time.

module DoubleListQueue : QUEUE =

struct

type ‘a queue = {front:’a list; rear:’a list}

let empty() = {front=[]; rear=[]}

let enqueuexq = {front=q.front; rear=x::q.rear}

let is_emptyq =

match q.front, q.rearwith

| [], [] -> true

| _, _ -> false

exception EmptyQueue

let deq (q:’a queue) : ‘a * ‘a queue =

match q.frontwith

| h::t -> (h, {front=t; rear=q.rear})

| [] -> match List.revq.rearwith

| h::t -> (h, {front=t; rear=[]})

| [] -> raise EmptyQueue

let dequeue (q:’a queue) : ‘a queue = snd(deqq)

let front (q:’a queue) : ‘a = fst(deqq)

end


In pictures

In pictures

let q0 = empty ();; {front=[];rear=[]}

let q1 = enqueue 3 q0;; {front=[];rear=[3]}

let q2 = enqueue 4 q1 ;; {front=[];rear=[4;3]}

let q3 = enqueue 5 q2 ;; {front=[];rear=[5;4;3]}

let q4 = dequeue q3 ;; {front=[4;5];rear=[]}

let q5 = dequeue q4 ;; {front=[5];rear=[]}

let q6 = enqueue 6 q5 ;; {front=[5];rear=[6]}

let q7 = enqueue 7 q6 ;; {front=[5];rear=[7;6]}


How would we design an abstraction

How would we design an abstraction?

  • Write some test cases:

    • what operations might you want?

    • what abstract types might you want?

  • From this, we can derive a signature

    • list the types

    • list the operations with their types

    • don’t forget to provide enough operations that you can debug!

  • Then we can build an implementation

    • when prototyping, build the simplest thing you can.

    • later, we can swap in a more efficient implementation.

    • (assuming we respect the abstraction barrier.)


Example directed graphs

Example: Directed graphs

  • What abstract types are involved?

    • nodes

    • edges

  • What operations might you want on graphs?

    • operations for building graphs

      • the empty graph

      • adding a new node to the graph

      • adding a new edge between two nodes in the graph

    • operations for observing graphs

      • what are the nodes in the graph?

      • given a node n in the graph, what nodes have an edge coming from n?


A possible signature

A possible signature

module type GRAPH =

sig

type node = int

type graph

valempty : unit -> graph

valadd_node : node -> graph -> graph

valadd_edge : node -> node -> graph -> graph

valall_nodes : graph -> node list

valneighbors : graph -> node -> node list

end


A possible implementation

A possible implementation

module SimpleGraph : GRAPH = struct

type node = int

type node_info = {nd:node ; edges:list node}

type graph = node_info list

let empty() = []

let add_nodeng = {nd=n ; edges=[]}::g

let all_nodesg = List.map (fun ni -> ni.nd) g

let neighbors gn =

(List.find (fun ni -> ni.nd = n) g).edges

let add_edge n1 n2 g =

List.map (fun ni ->

if ni.hd = n1 then

{nd=ni.hd; edges=n2::ni.edges}

else ni) g

end


Common interfaces

Common interfaces

The stack and queue interfaces are quite similar:

module type QUEUE =

sig

type ‘a queue

valempty : unit -> ‘a queue

valenqueue : ‘a -> ‘a queue -> ‘a queue

valis_empty : ‘a queue -> bool

exception EmptyQueue

valdequeue : ‘a queue -> ‘a queue

valfront : ‘a queue -> ‘a

end

module type STACK =

sig

type ‘a stack

valempty : unit -> ‘a stack

valpush : int -> ‘a stack -> ‘a stack

valis_empty : ‘a stack -> bool

exception EmptyStack

valpop : ‘a stack -> ‘a stack

valtop : ‘a stack -> ‘a

end


It s a good idea to factor out patterns

It’s a good idea to factor out patterns:

module type CONTAINER =

sig

type ‘a t

valempty : unit -> ‘a t

valinsert : ‘a -> ‘a t -> ‘a t

valis_empty : ‘a t -> bool

exception Empty

valremove_first: ‘a t -> ‘a t

valfirst : ‘a t -> ‘a

end

This lets us write an algorithm, like a tree traversal, using a generic container interface.

To get depth-first traversal, use the stack; to get breadth-first traversal, use the queue; to get prioritized traversal, use a priority queue; etc.


Matrices

Matrices

  • Suppose I ask you to write a generic package for matrices.

    • e.g., matrix addition, matrix multiplication

  • The package should be parameterized by the element type.

    • We may want to use ints or floats or complex numbers or binary values or … for the elements.


Ring signature

Ring signature

module type RING =

sig

type t

valzero : t

valone : t

valadd : t -> t -> t

valmul : t -> t -> t

end


Some rings

Some rings

module IntRing = module BoolRing =

structstruct

type t = inttype t = bool

let zero = 0 let zero = false

let one = 1 let one = true

let add = (+) let add xy = x || y

let mul = ( * ) let mulxy = x && y

end end

module FloatRing =

struct

type t = float

let zero = 0.0

let one = 1.0

let add = (+.)

let mul = ( *. )

end


Matrix signature

Matrix signature

module type MATRIX =

sig

type elt

type matrix

valmatrix_of_list : (elt list) list -> matrix

valadd : matrix -> matrix -> matrix

valmul : matrix -> matrix -> matrix

end


Matrix functor

Matrix functor

module DenseMatrix (R: RING) : (MATRIX with elt = R.t) =

struct

type elt = R.t

type matrix = (elt list) list

let matrix_of_list rows = rows

let add m1 m2 =

List.map (fun (r1,r2) ->

List.map (fun (e1,e2) -> R.add e1 e2))

(List.combine r1 r2)) (List.combine m1 m2)

let mul m1 m2 = (* good exercise *)

end

module IntMatrix = DenseMatrix(IntRing)

module FloatMatrix = DenseMatrix(FloatRing)

module BoolMatrix = DenseMatrix(BoolRing)


Matrix functor1

Matrix functor

DenseMatrix is a function from RING modules to MATRIX modules.

module DenseMatrix (R: RING) : (MATRIX with elt = R.t) =

struct

type elt = R.t

type matrix = (elt list) list

let matrix_of_list rows = rows

let add m1 m2 =

List.map (fun (r1,r2) ->

List.map (fun (e1,e2) -> R.add e1 e2))

(List.combine r1 r2)) (List.combine m1 m2)

let mul m1 m2 = (* good exercise *)

end

module IntMatrix = DenseMatrix(IntRing)

module FloatMatrix = DenseMatrix(FloatRing)

module BoolMatrix = DenseMatrix(BoolRing)


Matrix functor2

Matrix functor

The “with” is needed so that we reveal the element type (else the client will have to treat elements as abstract)

module DenseMatrix (R: RING) : (MATRIX with elt = R.t) =

struct

type elt = R.t

type matrix = (elt list) list

let matrix_of_list rows = rows

let add m1 m2 =

List.map (fun (r1,r2) ->

List.map (fun (e1,e2) -> R.add e1 e2))

(List.combine r1 r2)) (List.combine m1 m2)

let mul m1 m2 = (* good exercise *)

end

module IntMatrix = DenseMatrix(IntRing)

module FloatMatrix = DenseMatrix(FloatRing)

module BoolMatrix = DenseMatrix(BoolRing)


Matrix functor3

Matrix functor

In other words, clients are told that Matrix elt’s have the same type as the input RING’s t.

module DenseMatrix (R: RING) : (MATRIX with elt = R.t) =

struct

type elt = R.t

type matrix = (elt list) list

let matrix_of_list rows = rows

let add m1 m2 =

List.map (fun (r1,r2) ->

List.map (fun (e1,e2) -> R.add e1 e2))

(List.combine r1 r2)) (List.combine m1 m2)

let mul m1 m2 = (* good exercise *)

end

module IntMatrix = DenseMatrix(IntRing)

module FloatMatrix = DenseMatrix(FloatRing)

module BoolMatrix = DenseMatrix(BoolRing)


Matrix functor4

Matrix functor

Here we create a module for the different rings by applying the functor to appropriate module arguments.

module DenseMatrix (R: RING) : (MATRIX with elt = R.t) =

struct

type elt = R.t

type matrix = (elt list) list

let matrix_of_list rows = rows

let add m1 m2 =

List.map (fun (r1,r2) ->

List.map (fun (e1,e2) -> R.add e1 e2))

(List.combine r1 r2)) (List.combine m1 m2)

let mul m1 m2 = (* good exercise *)

end

module IntMatrix = DenseMatrix(IntRing)

module FloatMatrix = DenseMatrix(FloatRing)

module BoolMatrix = DenseMatrix(BoolRing)


Another functor example

Another functor example

module type BASE =

sig

valbase : int

end

moduleUbignumGenerator (Base: BASE) : UNSIGNED_BIGNUM =

struct

typeubignum = int list

lettoInt (b: ubignum) : int=

List.fold_left(funaccumcoeff-> coeff + Base.base * accum) 0 b

end

module Ubignum_1000 = UbignumGenerator(struct let base = 1000 end)

module Ubignum_10 = UbignumGenerator(struct let base = 10 end)

module Ubignum_2 = UbignumGenerator(struct let base = 2 end)


Sub t yping

Subtyping

  • A module matches any interface as long as it provides at least the definitions (of the right type) specified in the interface.

  • But as we saw earlier, the module can have more stuff.

    • e.g., the deq function in the Queue modules

  • Basic principle of sub-typing for modules:

    • wherever you are expecting a module with signature S, you can use a module with signature S’, as long as all of the stuff in S appears in S’.

    • That is, S’ is a bigger interface.


Groups versus rings

Groups versus rings

module type GROUP =

sig

type t

valzero : t

valadd : t -> t -> t

end

module type RING =

sig

type t

valzero : t

valone : t

valadd : t -> t -> t

valmul : t -> t -> t

end

module IntGroup : GROUP = IntRing

module FloatGroup : GROUP = FloatRing

module BoolGroup : GROUP = BoolRing


Groups versus rings1

Groups versus rings

RING is a sub-type of GROUP.

module type GROUP =

sig

type t

valzero : t

valadd : t -> t -> t

end

module type RING =

sig

type t

valzero : t

valone : t

valadd : t -> t -> t

valmul : t -> t -> t

end

module IntGroup : GROUP = IntRing

module FloatGroup : GROUP = FloatRing

module BoolGroup : GROUP = BoolRing


Groups versus rings2

Groups versus rings

There are more modules matching the GROUP interface than the RING one.

module type GROUP =

sig

type t

valzero : t

valadd : t -> t -> t

end

module type RING =

sig

type t

valzero : t

valone : t

valadd : t -> t -> t

valmul : t -> t -> t

end

module IntGroup : GROUP = IntRing

module FloatGroup : GROUP = FloatRing

module BoolGroup : GROUP = BoolRing


Groups versus rings3

Groups versus rings

Any Functor expecting a GROUP can be passed a RING.

module type GROUP =

sig

type t

valzero : t

valadd : t -> t -> t

end

module type RING =

sig

type t

valzero : t

valone : t

valadd : t -> t -> t

valmul : t -> t -> t

end

module IntGroup : GROUP = IntRing

module FloatGroup : GROUP = FloatRing

module BoolGroup : GROUP = BoolRing


Groups versus rings4

Groups versus rings

The include primitive is like cutting-and-pasting the signature’s content here.

module type GROUP =

sig

type t

valzero : t

valadd : t -> t -> t

end

module type RING =

sig

include GROUP

valone : t

valmul : t -> t -> t

end

module IntGroup : GROUP = IntRing

module FloatGroup : GROUP = FloatRing

module BoolGroup : GROUP = BoolRing


Groups versus rings5

Groups versus rings

That ensures we will be a sub-type of the included signature.

module type GROUP =

sig

type t

valzero : t

valadd : t -> t -> t

end

module type RING =

sig

include GROUP

valone : t

valmul : t -> t -> t

end

module IntGroup : GROUP = IntRing

module FloatGroup : GROUP = FloatRing

module BoolGroup : GROUP = BoolRing


A bigger example from moogle

A bigger example (from Moogle)

module type SET =

sig

type elt

type set

valempty : set

valis_empty : set -> bool

valinsert : elt -> set -> set

valsingleton : elt -> set

valunion : set -> set -> set

valintersect : set -> set -> set

valremove : elt -> set -> set

valmember : elt -> set -> bool

valchoose : set -> (elt * set) option

valfold : (elt -> 'a -> 'a) -> 'a -> set -> 'a end


Our set implementation is a functor

Our set implementation is a functor

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Our set implementation is a functor1

Our set implementation is a functor

ListSet is a parameterized module – given a module argument for Elt, it generates a new module.

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Our set implementation is a functor2

Our set implementation is a functor

This is a very simple, anonymous signature (it just specifies there’s some type t) for the argument to ListSet

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Our set implementation is a functor3

Our set implementation is a functor

This is the signature of the resulting module – we have a set plus the knowledge that the Set’s elt type is equal to Elt.t

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Our set implementation is a functor4

Our set implementation is a functor

These are two SET modules that I created with the ListSetfunctor.

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Our set implementation is a functor5

Our set implementation is a functor

In this case, I’m passing in an anonymous module for Elt that defines t to be int.

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Our set implementation is a functor6

Our set implementation is a functor

So we know that IntListSet.elt = int.

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Our set implementation is a functor7

Our set implementation is a functor

In this case, I’m passing in an anonymous module for Elt that defines t to be string.

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Our set implementation is a functor8

Our set implementation is a functor

So we know that StringListSet.elt = string.

module ListSet (Elt : sig type t end) : (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

...

end

module IntListSet = ListSet(struct type t = intend)

module StringListSet = ListSet(struct type t = string end)


Let s write the rest of the f unctor

Let’s write the rest of the functor

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

let empty : set = []

let is_empty (s:set) =

match xswith

| [] -> true

| _::_ -> false

let singleton (x:elt) : set = [x]

let insert (x:elt) (s:set) : set =

if List.memxsthen selse x::s

...

end


Let s write the rest of the functor

Let’s write the rest of the functor

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

...

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

let union (s1:set) (s2:set) : set = ???

end


Let s write the rest of the functor1

Let’s write the rest of the functor

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

...

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

let union (s1:set) (s2:set) : set =

s1 @ s2

...

end


Let s write the rest of the functor2

Let’s write the rest of the functor

Ugh. Wastes space if s1 and s2 have duplicates. (Also, makes remove harder…)

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

...

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

let union (s1:set) (s2:set) : set =

s1 @ s2

...

end


Let s write the rest of the functor3

Let’s write the rest of the functor

But it was damned easy to write!

Ugh. Wastes space if s1 and s2 have duplicates. (Also, makes remove harder…)

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

...

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

let union (s1:set) (s2:set) : set =

s1 @ s2

...

end


Let s write the rest of the functor4

Let’s write the rest of the functor

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

...

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

let union (s1:set) (s2:set) : set =

List.fold_right insert s1 s2

...

end


Let s write the rest of the functor5

Let’s write the rest of the functor

Gets rid of the duplicates. Now remove can stop once it finds the element.

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

...

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

let union (s1:set) (s2:set) : set =

List.fold_right insert s1 s2

...

end


Let s write the rest of the functor6

Let’s write the rest of the functor

But List.memand List.fold_right take time proportional to the length of the list. So union is quadratic.

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

...

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

let union (s1:set) (s2:set) : set =

List.fold_right insert s1 s2

...

end


Let s write the rest of the functor7

Let’s write the rest of the functor

If we knew that s1 and s2 were sorted we could use the merge from mergesort to compute the sorted union in

linear time.

module ListSet (Elt : sig type t end)

: (SET with elt = Elt.t) =

struct

type elt = Elt.t

type set = elt list

...

let insert (x:elt) (s:set) : set =

if List.mem x s then s else x::s

let union (s1:set) (s2:set) : set =

List.fold_right insert s1 s2

...

end


A sorted list set functor

A sorted list set functor

module type ELT = sig

type t

val compare: t -> t -> Order.order

end

module SortedListSet (Elt: ELT) : (SET with elt = Elt.t) =

struct

...

let rec insert (x: elt) (s: set) : set =

match s with

| [] -> [x]

| h::t -> (match Elt.comparexhwith

| Less -> x::s

| Eq-> s

| Greater -> h::insert x t)

...


A sorted list set functor1

A sorted list set functor

module type ELT = sig

type t

val compare: t -> t -> Order.order

end

module SortedListSet (Elt: ELT) : (SET with elt = Elt.t) =

struct

...

let rec union (s1: set) (s2: set) : set =

match s1, s2 with

| [], _ -> s2

| _, [] -> s1

| h1::t1, h2::t2 ->

(match Elt.compare h1 h2 with

| Less -> h1 :: union t1 s2

| Greater -> h2 :: union s1 t2

| Eq -> h1 :: union t1 t2)

...


A sorted list set functor2

A sorted list set functor

To support the sorting, I’m passing in a comparison operation to go with the element type.

module type ELT = sig

type t

val compare: t -> t -> Order.order

end

module SortedListSet (Elt : ELT) : (SET with elt = Elt.t) =

struct

...

let rec union (s1: set) (s2: set) : set =

match s1, s2 with

| [], _ -> s2

| _, [] -> s1

| h1::t1, h2::t2 ->

(match Elt.compare h1 h2 with

| Less -> h1 :: union t1 s2

| Greater -> h2 :: union s1 t2

| Eq -> h1 :: union t1 t2)

...


A sorted list set functor3

A sorted list set functor

Would union work correctly if we didn’t have sorted lists?

module type ELT = sig

type t

val compare: t -> t -> Order.order

end

module SortedListSet (Elt : ELT) : (SET with elt = Elt.t) =

struct

...

let rec union (s1: set) (s2: set) : set =

match s1, s2 with

| [], _ -> s2

| _, [] -> s1

| h1::t1, h2::t2 ->

(match Elt.compare h1 h2 with

| Less -> h1 :: union t1 s2

| Greater -> h2 :: union s1 t2

| Eq -> h1 :: union t1 t2)

...


A sorted list set functor4

A sorted list set functor

Clients can’t pass lists to union. They can only pass in sets (they don’t know sets are lists.) So if we ensure all of the constructors for sets sort the lists, we are guaranteed union will only see sorted lists!

module type ELT = sig

type t

val compare: t -> t -> Order.order

end

module SortedListSet (Elt : ELT) : (SET with elt = Elt.t) =

struct

...

let rec union (s1: set) (s2: set) : set =

match s1, s2 with

| [], _ -> s2

| _, [] -> s1

| h1::t1, h2::t2 ->

(match Elt.compare h1 h2 with

| Less -> h1 :: union t1 s2

| Greater -> h2 :: union s1 t2

| Eq -> h1 :: union t1 t2)

...


A sorted list set functor5

A sorted list set functor

This means we have far fewer test cases to write.

module type ELT = sig

type t

val compare: t -> t -> Order.order

end

module SortedListSet (Elt : ELT) : (SET with elt = Elt.t) =

struct

...

let rec union (s1: set) (s2: set) : set =

match s1, s2 with

| [], _ -> s2

| _, [] -> s1

| h1::t1, h2::t2 ->

(match Elt.compare h1 h2 with

| Less -> h1 :: union t1 s2

| Greater -> h2 :: union s1 t2

| Eq -> h1 :: union t1 t2)

...


Even simpler

Even simpler!

module type ELT = sig

type t

val compare: t -> t -> Order.order

end

module SortedListSet (Elt : ELT) : (SET with elt = Elt.t) =

struct

...

let rec union (s1: set) (s2: set) : set =

match s1, s2 with

| [], _ -> s2

| _, [] -> s1

| h1::t1, h2::t2 ->

(match Elt.compare h1 h2 with

| Less -> h1 :: union t1 s2

| Greater -> h2 :: union s1 t2

| Eq -> h1 :: union t1 t2)

let insert e s = union [e] s

...


An alternative bit vectors

An alternative: Bit vectors

module BitVectorSet (Elt : sig type t

valindex : t -> int

valmax : int

end) : (SET with elt = Elt.t) =

struct

type set = bool array

let empty = Array.createElt.max false

let member x s = s.(Elt.index x)

let union s1 s2 =

Array.initElt.max (fun i -> s1.(i) || s2.(i))

let intersect s1 s2 =

Array.initElt.max (fun i -> s1.(i) && s2.(i))

...


An alternative binary search t rees

An alternative: Binary search trees

module BSTreeSet (Elt : sig type t

valcompare : t -> t -> Order.order

end) : (SET with elt = Elt.t) =

struct

type set = Leaf | Node of set * elt * set

let empty = Leaf

let rec insert (x:elt) (s:set) : set =

match s with

| Leaf -> Node(Leaf,x,Leaf)

| Node(left,e,right) ->

(match Elt.compare x e with

| Eq -> s

| Less -> Node(insert x left, e, right)

| Greater -> Node(left, e, insert x right))

...


Binary search trees

Binary search trees

  • If the BST has n elements, and the tree is balanced, then the worst case time to do a member test is proportional to log2n.

    • Balanced: each node has left and right sub-trees of equal size.

    • Each time we encounter a node, we go left or right.

      • If the tree is balanced, this cuts out half of the nodes.

      • log2n is the number of times you can divide n by 2.

  • Alas, insert does not guarantee that the tree remains balanced.

    • In fact, it’s impossible to remain perfectly balanced.

    • But as long as the sub-trees are within a constant factor of each other’s size, the time to do member is still proprotional to log2n.


Example1

Example

insert 5, 7, 3, 9, 8, 2, 1


Example2

Example

insert 5, 7, 3, 9, 8, 2, 1

5


Example3

Example

insert 5, 7, 3, 9, 8, 2, 1

5

7


Example4

Example

insert 5, 7, 3, 9, 8, 2, 1

5

7

3


Example5

Example

insert 5, 7, 3, 9, 8, 2, 1

5

7

3

9


Example6

Example

insert 5, 7, 3, 9, 8, 2, 1

5

7

3

9

8


Example7

Example

insert 5, 7, 3, 9, 8, 2, 1

5

7

3

9

2

8


Example8

Example

insert 5, 7, 3, 9, 8, 2, 1

5

7

3

9

2

1

8


Bad scenario

Bad scenario

insert 1 2 3 4

1


Bad scenario1

Bad scenario

insert 1 2 3 4

1

2


Bad scenario2

Bad scenario

insert 1 2 3 4

1

2

3


Bad scenario3

Bad scenario

insert 1 2 3 4

1

2

3

4


Red black trees

Red-black trees

  • A clever way to keep the trees balanced as we insert.

    • insertion takes worst case time log2n in the size of the tree

    • member takes worst case time log2n.

    • Best of both worlds!

  • Each node is labeled Red or Black.

    • The colors provide local information so we can tell if/where we need to do some work to re-balance.

  • As we insert, we do rotations to keep the tree in balance.


Example red black tree

Example red-black tree

Note: I left the leaves off to fit things on a slide.

7

11

4

1

5

15

12

17

0

3


Key invariants

Key invariants

No red node has a red child node.

Every path from the root to a leaf has the same number of black nodes

7

11

4

1

5

15

12

17

0

3


So suppose we insert 25

So suppose we insert 25

7

11

4

1

5

15

12

17

0

3


So suppose we insert 251

So suppose we insert 25

7

11

4

1

5

15

12

17

25

0

3


So suppose we insert 252

So suppose we insert 25

7

Oops. Now a red node has a red child node.

11

4

1

5

15

12

17

25

0

3


So suppose we insert 253

So suppose we insert 25

We can’t make 25 black, because then there wouldn’t be equal numbers of black nodes on all paths.

7

11

4

1

5

15

12

17

25

0

3


Must rebalance

Must rebalance

7

11

4

1

5

15

12

17

25

0

3


Must rebalance1

Must rebalance

7

12

4

11

25

1

5

17

15

0

3


Must rebalance2

Must rebalance

Notice the 2 invariants are restored.

7

12

4

11

25

1

5

17

15

0

3


Must rebalance3

Must rebalance

The trick is efficiently finding some rebalancing.

7

12

4

11

25

1

5

17

15

0

3


Helper function 1 rotate left

Helper function 1: Rotate left

a

b

> b

b

< a

a

> a < b

> b

< a

> a < b


Helper function 2 rotate r ight

Helper function 2: Rotate right

a

b

> b

b

< a

a

> a < b

> b

< a

> a < b


Helper function 3 color flip

Helper function 3: Color flip

a

a

b

b

c

c

(and vice versa when a starts off as red)


Wrap up and summary

Wrap up and summary

  • It is often tempting to break the abstraction barrier.

    • e.g., during development, you want to print out a set, so you just call a convenient function you have lying around for iterating over lists and printing them out.

  • But the whole point of the barrier is to support future change in implementation.

    • e.g., moving from unsorted invariant to sorted invariant.

    • or from lists to balanced trees.

  • Many languages provide ways to leak information through the abstraction barrier.

    • “good” clients should not take advantage of this.

    • but they always end up doing it.

    • so you end up having to support these leaks when you upgrade, else you’ll break the clients.


Discussion

Discussion

  • It is often tempting to break the abstraction barrier.

    • e.g., during development, you want to print out a set, so you just call a convenient function you have lying around for iterating over lists and printing them out.

  • But the whole point of the barrier is to support future change in implementation.

    • e.g., moving from unsorted invariant to sorted invariant.

    • or from lists to balanced trees.

  • Many languages provide ways to leak information through the abstraction barrier.

    • “good” clients should not take advantage of this.

    • but they always end up doing it.

    • so you end up having to support these leaks when you upgrade, else you’ll break the clients.


Key points

Key points

  • Design in terms of abstract types and algorithms.

    • think “sets” not “lists” or “arrays” or “trees”

    • think “document” not “strings”

  • Use linguistic mechanisms to insulate clients from implementations of mechanisms.

    • makes it easy to swap in new implementations

    • the less you reveal in an interface, the easier it is to replace the implementation

    • on the other hand, you need to reveal enough in the interface to make it useful for clients.

  • In OCaml, we can use the module system

    • provides support for name-spaces

    • hiding information (types, local value definitions)

    • parameterization (functors) for re-use


Exercises modules interfaces for

Exercises: Modules & interfaces for:

  • dictionaries

    • insert a key and a value into a dictionary

    • lookup a key’s associated value in a dictionary

  • priority queues

    • similar to queues, but elements come with a priority

    • higher-priority elements dequeued first

  • bignums, rationals

    • arbitrary precision integers and rationals

    • associated operations (e.g., add, subtract, compare, etc.)

  • other matrix functors

    • array of arrays

    • array of list of non-zero values (sparse representation)

  • [un]directed-graphs

    • abstract over nodes and edges

    • operations for building graphs?

    • operations for querying graphs?

      • e.g., shortest path from a node A to a node B?


  • Login