<<

10/8/15

Hiding with functions procedural abstraction

Hiding details is the most important strategy for ML Modules writing correct, robust, reusable software. fun double x = x*2 and Abstract Data Types Can you tell the difference? fun double x = x+x val y = 2 Hiding implementation details is the most important strategy - double 4; fun double x = x*y for writing correct, robust, reusable software. fun double x = val it : int = 8 let fun help 0 y = y To p i s : | help x y = • ML and signatures. help (x-1) (y+1) • Abstraction for robust library and client+library code. in help x x end • Abstraction for easy change. “Private” top-level functions would also be nice... • ADTs and functions as data. • share a "private" helper

3 2

Name = signature NAME = struct bindings end sig binding-types end structure (module) signature namespace management and code organization type for a structure (module)

List of bindings and their types: structure MyMathLib = variables (incl. functions), type synonyms, datatypes, exceptions struct fun fact 0 = 1 Separate from specific structure. | fact x = x * fact (x-1) signature MATHLIB = val half_pi = Math.pi / 2 sig val fact : int -> int fun doubler x = x * 2 val half_pi : real end val doubler : int -> int end outside: val facts = List.map MyMathLib.fact [1,3,5,7,9]

4 5 adapted from slides by Dan Grossman

1 10/8/15

structure Name :> NAME = struct bindings end ascription Hiding with signatures (opaque – will ignore other kinds)

Ascribing a signature to a structure MyMathLib.doubler unbound (not in environment) outside module. • Structure must have all bindings with types as declared in signature. signature MATHLIB = signature MATHLIB2 = sig sig val fact : int -> int val fact : int -> int Real power: val half_pi : real val half_pi : real val doubler : int -> int Abstraction and Hiding end end structure MyMathLib2 :> MATHLIB2 = struct structure MyMathLib :> MATHLIB = fun fact 0 = 1 struct | fact x = x * fact (x-1) fun fact 0 = 1 val half_pi = Math.pi / 2.0 | fact x = x * fact (x-1) fun doubler x = x * 2 val half_pi = Math.pi / 2 end fun doubler x = x * 2 end 6 7

Abstract Library spec and invariants type of data and operations on it

External properties [externally visible guarantees, up to library writer] Example: rational numbers supporting add and toString • Disallow denominators of 0 • Return strings in reduced form (“4” not “4/1”, “3/2” not “9/6”) structure Rational = • No infinite loops or exceptions struct datatype rational = Whole of int Implementation invariants [not in external specification] | Frac of int*int • All denominators > 0 exception BadFrac • All rational values returned from functions are reduced (* see adts.ml for full code *) fun make_frac (x,y) = ... Signatures help enforce internal invariants. fun add (r1,r2) = ... fun toString r = ... end

8 9

2 10/8/15

More on invariants A first signature

With what we know so far, this signature makes sense: Our code maintains (and relies) on invariants. • Helper functions gcd and reduce not visible outside the mod u le.

Maintain: signature RATIONAL_OPEN = Attempt #1 • make_frac disallows 0 denominator, removes negative denominator, and sig reduces result datatype rational = Whole of int • add assumes invariants on inputs, calls reduce if needed | Frac of int*int exception BadFrac Rely: val make_frac : int * int -> rational • gcd assumes its arguments are non-negative val add : rational * rational -> rational • add uses math properties to avoid calling reduce val toString : rational -> string • toString assumes its argument is in reduced form end structure Rational :> RATIONAL_OPEN = ...

10 11

Problem: clients can violate invariants Solution: hide more!

ADT must hide concrete type definition so clients cannot Create values of type Rational.rational directly. create invariant-violating values of type directly. signature RATIONAL_OPEN = sig This attempt goes too far: type rational is not known to exist datatype rational = Whole of int | Frac of int*int ... signature RATIONAL_WRONG = Attempt #2 end sig exception BadFrac Rational.Frac(1,0) val make_frac : int * int -> rational Rational.Frac(3,~2) val add : rational * rational -> rational Rational.Frac(9,6) val toString : rational -> string end structure Rational :> RATIONAL_WRONG = ...

12 13

3 10/8/15

Abstract the type! (Really Big Deal!) Abstract Data Type of data + operations on it Client can pass them around, but can Type rationalexists, manipulate them only through module. but representation absolutely hidden. Outside of implementation: • Values of type rational can be signature RATIONAL = Success! (#3) created and manipulated only through ADT operations. sig • Concrete representation of values of type rational type rational Only way to make 1st rational. is absolutely hidden. exception BadFrac val make_frac : int * int -> rational Only operations signature RATIONAL = val add : rational * rational -> rational on rational. sig val toString : rational -> string end type rational exception BadFrac structure Rational :> RATIONAL = ... val make_frac : int * int -> rational val add : rational * rational -> rational val toString : rational -> string Module controls all operations with rational, end so client cannot violate invariants. 14 structure Rational :> RATIONAL = ... 15

Abstract Data Types: two key tools A cute twist

Powerful ways to use signatures for hiding: In our example, exposing the Whole constructor is no problem

In SML we can expose it as a function since the datatype binding in the 1. Deny bindings exist. module does create such a function Especially val bindings, fun bindings, constructors. • Still hiding the rest of the datatype • Still does not allow using Whole as a pattern 2. Make types abstract. signature RATIONAL_WHOLE = Clients cannot create or inspect values of the type directly. sig type rational exception BadFrac val Whole : int -> rational val make_frac : int * int -> rational val add : rational * rational -> rational val toString : rational -> string end 16 17

4 10/8/15

Signature matching rules Allow differentimplementations to be equivalent structure Struct :> SIG type-checks if and only if: A key purpose of abstraction: • No client can tell which you are using • Every non-abstract type in SIG is provided in Struct, as specified • Can improve/replace/choose later • Easier with more abstract signatures (reveal only what you must) • Every abstract type in SIG is provided in Struct in some way • Can be a datatype or a typ e s yno nym UnreducedRational in adts.sml. • Every val-binding in SIG is provided in Struct, possibly with a more • Same concrete datatype. general and/or less abstract internal type • • 'a list -> int more general than string list -> int Different invariant: reduce fractions only in toString. • example soon • Equivalent under RATIONAL and RATIONAL_WHOLE, but not under RATIONAL_OPEN. • Every exception in SIG is provided in Struct. PairRational in adts.sml. • Of course Struct can have more bindings (implicit in above rules) Different concrete datatype. • Equivalent under RATIONAL and RATIONAL_WHOLE, but cannot ascribe RATIONAL_OPEN. 18 19

PairRational (alternate concrete type) Some interesting details

• Internally has type , structure PairRational = make_frac int * int -> int * int externally int * int -> rational struct • Client cannot tell if we return argument unchanged type rational = int * int exception BadFrac • Internally Whole has type 'a -> 'a * int fun make_frac (x,y) = … externally int -> rational fun Whole i = (i,1) (* for RATIONAL_WHOLE *) • specialize 'a to int fun add ((a,b)(c,d)) = (a*d + b*c, b*d) • abstract int * int to rational fun toString r = ... (* reduce at last minute *) • Type-checker just figures it out end • Whole cannot have types 'a -> int * int or 'a -> rational (must specialize all 'a uses)

20 21

5 10/8/15

Cannot mix and match module bindings ADT (set.sml) signature SET = Common idiom: if module provides sig one externally visible type, name it t. Modules with the same signatures still define different types Then outside references are Set.t. type 'a t val empty : 'a t These do not type-check: val singleton : 'a -> 'a t • Rational.toString(UnreducedRational.make_frac(9,6)) val fromList : 'a list -> 'a t • PairRational.toString(UnreducedRational.make_frac(9,6)) val toList : 'a t -> 'a list val fromPred : (''a -> bool) -> ''a t Crucial for and module properties: val toPred : ''a t -> ''a -> bool • Different modules have different internal invariants! val toString : ('a -> string) -> 'a t -> string • ... and different type definitions: val isEmpty : 'a t -> bool • UnreducedRational.rational looks like Rational.rational, val member : 'a -> 'a t -> bool but clients and the type-checker do not know that val insert : 'a -> 'a t -> 'a t • PairRational.rational is int*int not a datatype! val delete : 'a -> 'a t -> 'a t val union : 'a t -> 'a t -> 'a t Will return and contrast with Object-Oriented techniques. val intersect : 'a t -> 'a t -> 'a t val diff : 'a t -> 'a t -> 'a t 22 end 23

Implementing the SET signature Sets are fun! Math: { x | x mod 3 = 0 } ListSet structure Represent sets as lists. Invariants? SML: fn x => x mod 3 = 0 • Duplicates? structure FunSet :> SET = • Ordering? sig type 'a t val empty = fn _ => false FunSet structure fun singleton x = fn y => x=y Represent sets as function closures (!!!) fun member x set = set x val insert x set = fn y => x=y orelse set y ... end

24 Are all set operations possible? 25

6