SLIDE 1 Two approaches to writing interfaces
Interface “projected” from implementation:
- No separate interface
- Compiler extracts from implementation
(CLU, Java class, Haskell)
- When code changes, must extract again
- Few cognitive benefits
Full interfaces:
- Distinct file, separately compiled
(Caml, Java interface, Modula, Ada)
- Implementations can change independently
- Full cognitive benefits
SLIDE 2 ML module terminology
Interface is a signature Implementation is a structure Generic module is a functor
- A compile-time function over structures
- The point: reuse without violating abstraction
Structures and functors match signature Analogy: Signatures are the “types” of structures.
SLIDE 3
Signature says what’s in a structure
Specify types (w/kind), values (w/type), exceptions. Ordinary type examples: type t // abstract type, kind * eqtype t type t = ... // ’manifest’ type datatype t = ... Type constructors work too type ’a t // abstract, kind * => * eqtype ’a t type ’a t = ... datatype ’a t = ...
SLIDE 4
Signature example: Ordering
signature ORDERED = sig type t val lt : t * t -> bool val eq : t * t -> bool end
SLIDE 5 Signature example: Integers
signature INTEGER = sig eqtype int (* <-- ABSTRACT type *) val ˜ : int -> int val + : int * int -> int val - : int * int -> int val * : int * int -> int val div : int * int -> int val mod : int * int -> int val > : int * int -> bool val >= : int * int -> bool val < : int * int -> bool val <= : int * int -> bool val compare : int * int -> order val toString : int
val fromString : string -> int option end
SLIDE 6
Implementations of integers
A selection. . . structure Int :> INTEGER structure Int31 :> INTEGER (* optional *) structure Int32 :> INTEGER (* optional *) structure Int64 :> INTEGER (* optional *) structure IntInf :> INTEGER (* optional *)
SLIDE 7
What about natural numbers?
signature NATURAL = sig type nat (* abstract, NOT ’eqtype’ *) exception Negative exception BadDivisor val of_int : int -> nat val /+/ : nat * nat -> nat val /-/ : nat * nat -> nat val /*/ : nat * nat -> nat val sdiv : nat * int -> { quotient : nat, remainder : int } val compare : nat * nat -> order val decimals : nat -> int list end
SLIDE 8
Signatures collect declarations
signature QUEUE = sig type ’a queue (* another abstract type *) exception Empty val empty : ’a queue val put : ’a * ’a queue -> ’a queue val get : ’a queue -> ’a * ’a queue (* raises Empty *) (* LAWS: get(put(a, empty)) == (a, empty) ... *) end
SLIDE 9
Structures collect definitions
structure Queue :> QUEUE = struct (* opaque seal *) type ’a queue = ’a list exception Empty val empty = [] fun put (x,q) = q @ [x] fun get [] = raise Empty | get (x :: xs) = (x, xs) (* LAWS: get(put(a, empty)) == (a, empty) ... *) end
SLIDE 10
Your turn! Signature for a stack
structure Stack = struct type ’a stack = ’a list exception Empty val empty = [] val push = op :: fun pop [] = raise Empty | pop (top :: rest) = (top, rest) end
SLIDE 11
Your turn! Signature for a stack
signature STACK = sig type ’a stack exception Empty val empty : ’a stack val push : ’a * ’a stack -> ’a stack val pop : ’a stack -> ’a * ’a stack end
SLIDE 12
Dot notation to access elements
structure Queue :> QUEUE = struct type ’a queue = ’a list exception Empty val empty = [] fun put (q, x) = q @ [x] fun get [] = raise Empty | get (x :: xs) = (x, xs) end fun single (x:’a) : ’a Queue.queue = Queue.put(Queue.empty, x)
SLIDE 13 What interface with what implementation?
Maybe mixed together, extracted by compiler!
Maybe matched by name:
Best: any interface with any implementation:
But: not “any”—only some matches are OK
SLIDE 14 Signature Matching
Well-formed structure Queue :> QUEUE = QueueImpl if principal signature of QueueImpl matches ascribed signature QUEUE:
- Every type in QUEUE is in QueueImpl
- Every exception in QUEUE is in QueueImpl
- Every value in QUEUE is in QueueImp
(type could be more polymorphic)
- Every substructure matches, too (none here)
SLIDE 15 Signature Ascription
Ascription attaches signature to structure
- Transparent Ascription: types are revealed
structure strid : sig_exp = struct_exp This method is stupid and broken (legacy) (But it’s awfully convenient)
- Opaque Ascription: types are hidden (“sealing”)
structure strid :> sig_exp = struct_exp This method respects abstraction (And when you need to expose, can be tiresome) Slogan: “use the beak”
SLIDE 16 Transparent Ascription
Not recommended! Example: structure IntLT : ORDERED = struct type t = int val le = (op <) val eq = (op =) end Exposed: IntLT.t = int
SLIDE 17 Opaque Ascription
Recommended Example:
structure Queue :> QUEUE = struct type ’a queue = ’a list exception Empty val empty = [] fun put (x, q) = q @ [x] fun get [] = raise Empty | get (x :: xs) = (x, xs) end
Not exposed: 'a Queue.queue = 'a list
SLIDE 18 How opaque ascription works
Outside module, no access to representation
- Protects invariants
- Allows software to evolve
- Type system limits interoperability
Inside module, complete access to representation
- Every function sees rep of every argument
- Key distinction abstract type vs object
SLIDE 19 Abstract data types and your homework
Two-player games:
- Abstraction not as crisp as “number” or “queue”
Problems abstraction must solve:
- Interact with human player via strings
(accept moves, display progress)
- Know whose turn it is
- Handle special features like “extra moves”
- Provide API for computer player
Result: a very wide interface
SLIDE 20 Abstraction design: Computer player
Computer player should work with any game, provided
- Up to two players
- Complete information
- Always terminates
Brute force: exhaustive search Your turn! What does computer player need?
- Types?
- Exceptions?
- Functions?
SLIDE 21
Our computer player: AGS
Any game has two key types:
type config structure Move : sig type move ... (* string conversion, etc *) end
Key functions use both types:
val possmoves : config -> Move.move list val makemove : config -> Move.move -> config
Multiple games with different config, move? Yes! Using key feature of ML: functor