Example interface with declarations signature ILIST = sig type a - - PowerPoint PPT Presentation

example interface with declarations
SMART_READER_LITE
LIVE PREVIEW

Example interface with declarations signature ILIST = sig type a - - PowerPoint PPT Presentation

Example interface with declarations signature ILIST = sig type a ilist (* nonempty; one element indicated *) val singletonOf : a -> a ilist val indicated : a ilist -> a val indicatorLeft : a ilist -> a ilist


slide-1
SLIDE 1

Example interface with declarations

signature ILIST = sig type ’a ilist (* nonempty; one element indicated *) val singletonOf : ’a -> ’a ilist val indicated : ’a ilist -> ’a val indicatorLeft : ’a ilist -> ’a ilist val indicatorRight : ’a ilist -> ’a ilist val deleteLeft : ’a ilist -> ’a ilist val deleteRight : ’a ilist -> ’a ilist val insertLeft : ’a * ’a ilist -> ’a ilist val insertRight : ’a * ’a ilist -> ’a ilist val ifoldl : (’a * ’b -> ’b) -> ’b -> ’a ilist -> ’b val ifoldr : (’a * ’b -> ’b) -> ’b -> ’a ilist -> ’b end

slide-2
SLIDE 2

Design choice: placing 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-3
SLIDE 3

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-4
SLIDE 4

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-5
SLIDE 5

Signature example: Ordering

signature ORDERED = sig type t val lt : t * t -> bool val eq : t * t -> bool end

slide-6
SLIDE 6

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

  • > string

val fromString : string -> int option end

slide-7
SLIDE 7

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-8
SLIDE 8

Homework: natural numbers

signature NATURAL = sig type nat (* abstract, NOT ’eqtype’ *) exception Negative exception BadDivisor val ofInt : 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 decimal : nat -> int list end

slide-9
SLIDE 9

Homework: integers

signature BIGNUM = sig type bigint exception BadDivision val ofInt : int -> bigint val negated : bigint -> bigint val <+> : bigint * bigint -> bigint val <-> : bigint * bigint -> bigint val <*> : bigint * bigint -> bigint val sdiv : bigint * int -> { quotient : bigint, remainder : int } val compare : bigint * bigint -> order val toString : bigint -> string end

slide-10
SLIDE 10

Signature review: 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-11
SLIDE 11

Structure: 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-12
SLIDE 12

Dot notation to access components

fun single x = Queue.put (Queue.empty, x) val _ = single : ’a -> ’a Queue.queue

slide-13
SLIDE 13

What interface with what implementation?

Maybe mixed together, extracted by compiler!

  • CLU, Haskell

Maybe matched by name:

  • Modula-2, Modula-3, Ada

Best: any interface with any implementation:

  • Java, Standard ML

But: not “any”—only some matches are OK

slide-14
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
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
SLIDE 16

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: type ’a Queue.queue = ’a list

  • Respects abstraction
slide-17
SLIDE 17

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-18
SLIDE 18

Abstract data types and your homework

Natural numbers

  • Funs/+/,/-/,/*/ see both representations
  • Makes arithmetic relatively easy
  • But type nat works only with type nat

(no “mixed” arithmetic)

slide-19
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, visualize state)

  • Know whose turn it is
  • Handle special features like “extra moves”
  • Provide API for computer player

Result: a wide interface

slide-20
SLIDE 20

Testing code with abstract types

Test properties of observed data:

  • If player X has won, the game is over
  • If the game is over, there are no legal moves
  • If there are no legal moves, the game is over

Same story with numbers:

  • negated (negated i) equals i
  • (i <+> j) <-> i equals j
slide-21
SLIDE 21

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-22
SLIDE 22

Our computer player: AGS

Any game has two key types:

type state structure Move : MOVE (* exports ‘move‘ *)

Key functions use both types:

val legalmoves : state -> Move.move list val makemove : state -> Move.move -> state

Multiple games with different state, move? Yes! Using key feature of ML: functor

slide-23
SLIDE 23

A functor is a generic module

A new form of parametric polymorphism:

  • lambda and type-lambda in one mechanism
  • Introduction form is functor (definition form)
  • Actually pleasant to use

“Generics” found across language landscape (wherever large systems are built)

slide-24
SLIDE 24

Game interoperability with functors

functor AgsFun (structure Game : GAME) :> sig structure Game : GAME val advice : Game.state -> { recommendation : Game.Move.move option , expectedOutcome : Player.outcome } end where type Game.Move.move = Game.Move.move and type Game.state = Game.state = struct structure Game = Game ... definitions of helpers, ‘advice‘ ... end

slide-25
SLIDE 25

Functors: baby steps

A functor abstracts over a module Formal parameters are declarations: functor MkSingle(structure Q:QUEUE) = struct structure Queue = Q fun single x = Q.put (Q.empty, x) end Combines familiar ideas:

  • Higher-order functions (value parameter Q.put)
  • type-lambda (type parameter Q.queue)
slide-26
SLIDE 26

Using Functors

Functor applications are evaluated at compile time. functor MkSingle(structure Q:QUEUE) = struct structure Queue = Q fun single x = Q.put (Q.empty, x) end Actual parameters are definitions

structure QueueS = MkSingle(structure Q = Queue) structure EQueueS = MkSingle(structure Q = EQueue)

where EQueue is a more efficient implementation

slide-27
SLIDE 27

Refining signature using where type

signature ORDER = sig type t val compare : t * t -> order end signature MAP = sig type key type ’a table val insert : key -> ’a -> ’a table -> ’a table ... end

functor RBTree(structure O:ORD) :> MAP where type key = O.t = struct ... end

slide-28
SLIDE 28

Versatile functors

Code reuse. RBTree with different orders Type abstraction. RBTree with different ordered types Separate compilation. RBTree compiled independently

functor RBTree(structure O:ORD) :> MAP where type key = O.t = struct ... end

slide-29
SLIDE 29

Functors on your homework

Separate compilation:

  • Unit tests for natural numbers, without an

implementation of natural numbers Code reuse with type abstraction

  • Abstract Game Solver

(any representation of game state, move)

slide-30
SLIDE 30

ML module summary

New syntactic category: declaration

  • Of type, value, exception, or module

Signature groups declarations: interface Structure groups definitions: implementation Functor enables reuse:

  • Formal parameter: declarations
  • Actual parameter: definitions

Opaque ascription hides information

  • Enforces abstraction
slide-31
SLIDE 31

Reusable Abstractions: Extended Example

Error-tracking interpreter for a toy language

slide-32
SLIDE 32

Classic “accumulator” for errors

signature ERROR = sig type error (* a single error *) type summary (* summary of what errors occurred *) val nothing : summary (* no errors *) val <+> : summary * summary -> summary (* combine *) val oneError : error -> summary (* laws: nothing <+> s == s s <+> nothing == s s1 <+> (s2 <+> s3) == (s1 <+> s2) <+> s3 // associativity *) end

slide-33
SLIDE 33

First Error: Implementation

structure FirstError :> ERROR where type error = string and type summary = string option = struct type error = string type summary = string option val nothing = NONE fun <+> (NONE, s) = s | <+> (SOME e, _) = SOME e val oneError = SOME end

slide-34
SLIDE 34

All Errors: Implementation

structure AllErrors :> ERROR where type error = string and type summary = string list = struct type error = string type summary = error list val nothing = [] val <+> = op @ fun oneError e = [e] end

slide-35
SLIDE 35

Exercise: Simple arithmetic interpreter

(* Given: *) datatype ’a comp = OK of ’a | ERR of AllErrors.summary datatype exp = LIT

  • f int

| PLUS of exp * exp | DIV

  • f exp * exp

(* Write an evaluation function that tracks errors. *) val eval : exp -> int comp = ...

slide-36
SLIDE 36

Exercise: LIT and PLUS cases

fun eval (LIT n) = OK n | eval (PLUS (e1, e2)) = (case eval e1

  • f OK v1 =>

(case eval e2

  • f OK

v2 => OK (v1 + v2) | ERR s2 => ERR s2) | ERR s1 => (case eval e2

  • f OK

_ => ERR s1 | ERR s2 => ERR (AllErrors.<+> (s1, s2))))

slide-37
SLIDE 37

Exercise: DIV case

| eval (DIV (e1, e2)) = (case eval e1

  • f OK v1 =>

(case eval e2

  • f OK

0 => ERR (AllErrors.oneError "Div 0") | OK v2 => OK (v1 div v2) | ERR s2 => ERR s2) | ERR s1 => (case eval e2

  • f OK

v2 => ERR s1 | ERR s2 => ERR (AllErrors.<+> (s1, s2)))

slide-38
SLIDE 38

Combining generic computations

signature COMPUTATION = sig type ’a comp (* Computation! When run, results in value of type ’a or error summary. *) (* A computation without errors always succeeds. *) val succeeds : ’a -> ’a comp (* Apply a pure function to a computation. *) val <$> : (’a -> ’b) * ’a comp -> ’b comp (* Application inside computations. *) val <*> : (’a -> ’b) comp * ’a comp -> ’b comp (* Computation followed by continuation. *) val >>= : ’a comp * (’a -> ’b comp) -> ’b comp end

slide-39
SLIDE 39

Buckets of generic algebraic laws

succeeds a >>= k == k a // identity comp >>= succeeds == comp // identity comp >>= (fn x => k x >>= h) == (comp >>= k) >>= h // associativity succeeds f <*> succeeds x == succeeds (f x) // success ...

slide-40
SLIDE 40

Environments using “computation”

signature COMPENV = sig type ’a env (* environment mapping strings to values of type ’a *) type ’a comp (* computation of ’a or an error summary *) val lookup : string * ’a env -> ’a comp end

slide-41
SLIDE 41

Payoff!

functor InterpFn(structure Error : ERROR structure Comp : COMPUTATION structure Env : COMPENV val zerodivide : Error.error val error : Error.error -> ’a Comp.comp sharing type Comp.comp = Env.comp) = struct val (<*>, <$>, >>=) = (Comp.<*>, Comp.<$>, Comp.>>=) (* Definition of Interpreter *) end

slide-42
SLIDE 42

Definition of intepreter, continued

datatype exp = LIT of int | VAR of string | PLUS of exp * exp | DIV

  • f exp * exp

fun eval (e, rho) = let fun ev(LIT n) = Comp.succeeds n | ev(VAR x) = Env.lookup (x, rho) | ev(PLUS (e1, e2)) = curry op + <$> ev e1 <*> ev e2 | ev(DIV (e1, e2)) = ev e1 >>= (fn n1 => ev e2 >>= (fn n2 => case n2

  • f 0 => error zerodivide

| _ => Comp.succeeds (n1 div n2))) in ev e end

slide-43
SLIDE 43

“Computation” abstraction is a “monad”

Supported by special syntax in Haskell:

eval :: Exp -> Hopefully Int eval (LIT v) = return v eval (PLUS e1 e2) = do { v1 <- eval e1 ; v2 <- eval e2 ; return (v1 + v2) } eval (DIV e1 e2) = do { v1 <- eval3 e1 ; v2 <- eval3 e2 ; if v2 == 0 then Error "div 0" else return (v1 ‘div‘ v2) }

slide-44
SLIDE 44

Extend a signature with include

signature ERRORCOMP = sig include COMPUTATION structure Error : ERROR datatype ’a result = OK

  • f ’a

| ERR of Error.summary val run : ’a comp -> ’a result val error : Error.error -> ’a comp end

slide-45
SLIDE 45

Let’s build ERRORCOMP

functor ErrorCompFn(structure Error : ERROR) :> ERRORCOMP where type Error.error = Error.error and type Error.summary = Error.summary = struct structure Error = Error datatype ’a result = OK

  • f ’a

| ERR of Error.summary type ’a comp = ’a result fun run comp = comp fun error e = ERR (Error.oneError e) fun succeeds = OK ... end

slide-46
SLIDE 46

ML module summary

New syntactic category: declaration

  • Of type, value, exception, or module

Signature groups declarations: interface Structure groups definitions: implementation Functor enables reuse:

  • Formal parameter: declarations
  • Actual parameter: definitions

Opaque ascription hides information

  • Enforces abstraction