SML Modules and Overview of Modules and ADTs Hiding implementa-on details is the most important strategy Abstract Data Types (ADTs) for wriCng correct, robust, reusable soEware.

Topics: These slides are lightly edited versions of Ben Wood’s Fall ‘15 slides, some of which are based on Dan Grossman’s material from the University of Washington. • ML structures and signatures. • AbstracCon for robust library and client+library code. • AbstracCon for easy change. • ADTs and funcCons as data. CS251 Programming Languages Spring 2019, Lyn Turbak

Department of Computer Science Wellesley College

SML Modules and ADTS 2

structure Name = struct bindings end Hiding with funcLons structure (module) procedural abstrac.on namespace management and code organizaLon structure MyMathLib = Hiding implementaCon details is the most important strategy for struct wriCng correct, robust, reusable soEware. fun fact 0 = 1 | fact x = x * fact (x-1) fun double x = x*2 Can you tell the difference? fun double x = x+x val half_pi = Math.pi / 2

val y = 2 - double 4; fun double x = x*y fun doubler x = x * 2 fun double x = val it : int = 8 let fun help 0 y = y val twelve = doubler (fact 3) | help x y = end help (x-1) (y+1) in help x x end outside:

“Private” top-level funcCons would also be nice... val facts = List.map MyMathLib.fact • share a "private" helper funcCon [1,4,MyMathLib.doubler 3, MyMathLib.twelve]

SML Modules and ADTS 3 SML Modules and ADTS 4 signature NAME = structure Name :> NAME = sig binding-types end ascripLon struct bindings end signature (opaque – will ignore other kinds) type for a structure (module) Ascribing a signature to a structure • Structure must have all bindings with types as declared in signature. List of bindings and their types: signature MATHLIB = variables (incl. funcCons), type synonyms, datatypes, excepCons sig Separate from specific structure. val fact : int -> int Real power: val half_pi : real val doubler : int -> int Abstrac-on and Hiding signature MATHLIB = val twelve : int sig end val fact : int -> int val half_pi : real structure MyMathLib :> MATHLIB = val doubler : int -> int struct val twelve : int fun fact 0 = 1 end | fact x = x * fact (x-1) val half_pi = Math.pi / 2 fun doubler x = x * 2 val twelve = doubler (fact 3) SML Modules and ADTS 5 end SML Modules and ADTS 6

Hiding with signatures type of data and operaLons on it MyMathLib.doubler unbound (not in environment) outside module. Example: raConal numbers supporCng add and toString signature MATHLIB2 = sig structure Rational = val fact : int -> int struct val half_pi : real datatype rational = Whole of int val twelve : int | Frac of int*int end exception BadFrac structure MyMathLib2 :> MATHLIB2 = (* see rationals.sml for full code *) struct fun fact 0 = 1 fun make_frac (x,y) = ... | fact x = x * fact (x-1) fun add (r1,r2) = ... val half_pi = Math.pi / 2.0 fun toString r = ... fun doubler x = x * 2 end fun twelve = doubler (fact 3) end SML Modules and ADTS 7 SML Modules and ADTS 8

Library spec and invariants More on invariants

External properCes [externally visible guarantees, up to library writer] • Disallow denominators of 0 Our code maintains (and relies) on invariants. • Return strings in reduced form (“4” not “4/1”, “3/2” not “9/6”) • No infinite loops or excepCons Maintain: • make_frac disallows 0 denominator, removes negaCve denominator, and ImplementaCon invariants [not in external specifica9on] reduces result • All denominators > 0 • add assumes invariants on inputs, calls reduce if needed • All rational values returned from funcCons are reduced Rely: • gcd assumes its arguments are non-negaCve Signatures help enforce internal invariants. • add uses math properCes to avoid calling reduce • toString assumes its argument is in reduced form

SML Modules and ADTS 9 SML Modules and ADTS 10

A first signature Problem: clients can violate invariants

With what we know so far, this signature makes sense: • Helper funcCons gcd and reduce not visible outside the module. Create values of type Rational.rational directly.

signature RATIONAL_CONCRETE = signature RATIONAL_CONCRETE = A7empt #1 sig sig datatype rational = Whole of int datatype rational = Whole of int | Frac of int*int | Frac of int*int ... exception BadFrac end val make_frac : int * int -> rational val add : rational * rational -> rational Rational.Frac(1,0) val toString : rational -> string Rational.Frac(3,~2) end Rational.Frac(40,32) structure Rational :> RATIONAL_OPEN = ...

SML Modules and ADTS 11 SML Modules and ADTS 12 SoluLon: hide more! Abstract the type! (Really Big Deal!) Client can pass them around, but can ADT must hide concrete type defini5on so clients cannot Type rational exists, manipulate them only through module. create invariant-viola5ng values of type directly. but representa-on absolutely hidden.

signature RATIONAL = Success! (#3) This agempt goes too far: type rational is not known to exist sig type rational Only way to make 1st rational. exception BadFrac signature RATIONAL_WRONG = A7empt #2 Only opera-ons val make_frac : int * int -> rational sig on rational. val add : rational * rational -> rational exception BadFrac val toString : rational -> string val make_frac : int * int -> rational end val add : rational * rational -> rational val toString : rational -> string structure Rational :> RATIONAL = ...

end

structure Rational :> RATIONAL_WRONG = ... Module controls all opera-ons with rational, so client cannot violate invariants. SML Modules and ADTS 13 SML Modules and ADTS 14

Abstract Data Type Abstract type of data + operaLons on it Abstract Data Types: two key tools Outside of implementaCon: • Values of type rational can be Powerful ways to use signatures for hiding: created and manipulated only through ADT opera-ons.

• Concrete representa-on of values of type rational is absolutely hidden. 1. Deny bindings exist. Especially val bindings, fun bindings, constructors. signature RATIONAL = sig 2. Make types abstract. type rational Clients cannot create or inspect values of the type directly. exception BadFrac val make_frac : int * int -> rational val add : rational * rational -> rational val toString : rational -> string end structure Rational :> RATIONAL = ...

SML Modules and ADTS 15 SML Modules and ADTS 16 A cute twist Signature matching rules

In our example, exposing the Whole constructor is no problem structure Struct :> SIG type-checks if and only if:

In SML we can expose it as a funcCon since the datatype binding in the • Every non-abstract type in SIG is provided in Struct, as specified module does create such a funcCon • SCll hiding the rest of the datatype • Every abstract type in SIG is provided in Struct in some way • SCll does not allow using Whole as a pagern • Can be a datatype or a type synonym signature RATIONAL_WHOLE = • Every val-binding in SIG is provided in Struct, possibly with a more sig general and/or less abstract internal type type rational • 'a list -> int more general than string list -> int • example soon exception BadFrac val Whole : int -> rational • Every excepCon in SIG is provided in Struct. val make_frac : int * int -> rational val add : rational * rational -> rational val toString : rational -> string Of course Struct can have more bindings (implicit in above rules) end

SML Modules and ADTS 17 SML Modules and ADTS 18

PairRaLonal (alternaLve concrete type) Allow different implementa.ons to be equivalent

A key purpose of abstracCon: structure PairRational = • No client can tell which you are using struct • Can improve/replace/choose implementaCons later type rational = int * int • Easier with more abstract signatures (reveal only what you must) exception BadFrac

fun make_frac (x,y) = … UnreducedRational in adts.sml. fun Whole i = (i,1) (* for RATIONAL_WHOLE *) • Same concrete datatype. fun add ((a,b)(c,)) = (a*d + b*c, b*d) • Different invariant: reduce fracCons only in toString. fun toString r = ... (* reduce at last minute *) • Equivalent under RATIONAL and RATIONAL_WHOLE, end but not under RATIONAL_CONCRETE.

PairRational in adts.sml. • Different concrete datatype. • Equivalent under RATIONAL and RATIONAL_WHOLE, but cannot ascribe RATIONAL_CONCRETE.

SML Modules and ADTS 19 SML Modules and ADTS 20 Some interesLng details Cannot mix and match module bindings

• Internally make_frac has type int * int -> int * int, Modules with the same signatures sCll define different types externally int * int -> rational • Client cannot tell if we return argument unchanged These do not type-check: • Rational.toString(UnreducedRational.make_frac(9,6)) • Internally Whole has type 'a -> 'a * int • PairRational.toString(UnreducedRational.make_frac(9,6)) externally int -> rational • specialize 'a to int Crucial for and module properLes: • abstract int * int to rational • Type-checker just figures it out • Different modules have different internal invariants! • ... and different type definiLons: • Whole cannot have types 'a -> int * int • UnreducedRational.rational looks like Rational.rational, but or 'a -> rational (must specialize all 'a uses) clients and the type-checker do not know that • PairRational.rational is int*int not a datatype!

SML Modules and ADTS 21 SML Modules and ADTS 22

Double Ccks mean a Is an equality Set ADT (set.sml) type (can compare elts with =) Side Note: Equality Types Double-9ck types like range over so-called equality types, which are types over signature SET = Common idiom: if module provides ''a which the polymorphic equality operator = is defined. sig one externally visible type, name it t. type ''a t Then outside references are Set.t. Sadly, the semanCcs of IEEE 754 floaCng point arithmeCc standard prevents the real val empty : ''a t type from being an equality type. It includes Nan (not-a-number) values that represent val singleton : ''a -> ''a t the results of certain operaCons, such as subtracCng posiCve infinity from itself. val isEmpty : ''a t -> bool According to the IEEE standard, tesCng two Nan values for equality must return false, but that would break the reflexivity property that is required for an equality type (i.e., val size : ''a t -> int for any value v in an equality type, v = v must be true). See the examples below. val member : ''a -> ''a t -> bool val insert : ''a -> ''a t -> ''a t ;val myNan = Real.posInf - Real.posInf; - myNan = myNan - val delete : ''a -> ''a t -> ''a t val myNan = nan : real Error: operator and operand val union : ''a t -> ''a t -> ''a t don't agree [equality type val intersection : ''a t -> ''a t -> ''a t - Real.isNan myNan; required] val difference : ''a t -> ''a t -> ''a t val it = true : bool operator domain: ''Z * ''Z operand: real * real val fromList : ''a list -> ''a t - Real.==(myNan,myNan); val toList : ''a t -> 'a list val it = false : bool - Real.compare(myNan,myNan) val fromPred : (''a -> bool) -> ''a t uncaught exception Unordered val toPred : ''a t -> ''a -> bool - Real.compareReal(myNan,myNan); val toString : (''a -> string) -> ''a t -> string val it = UNORDERED : IEEEReal.real_order end SML Modules and ADTS 23 SML Modules and ADTS 24

ImplemenLng the SET signature ListSet (in class; soluLons in SML VM repo) ListSet structure (in class) structure ListSet :> SET = Represent sets as unordered list. struct • Invariant: no duplicates type ''a t = ''a list • What about ordering? Can’t use it, since not part val empty = [] of signature! fun singleton x = [x] fun insert x ys = ListSetDups structure (in class) if member x ys then ys else x :: ys ... flesh out the rest in class ... Represent sets as unordered list, *allowing* duplicates end FunSet structure (PS8) • Represent sets as unordered list without duplicates Represent sets as predicate funcCons • Can’t use ordering, since not part of signature! • The following are helpful in implementaCon: Opera-onTreeSet structure (PS8) foldr, List.filter, List.exists, Represent sets as trees of set operaCon SML Modules and ADTS 25 String.concatWith SML Modules and ADTS 26

Opening Modules TesLng ListSet ;(ListSet.isEmpty (ListSet.empty - ;[val it = true : bool - val s1 = fromList [1,2,1,2,3,2,3,1,4 val s1 = - : int t

- ListSet.size (ListSet.singleton 17); - toList s1; val it = 1 : int val it = [4,3,2,1] : int list

- open ListSet; - toString Int.toString s1; val it = "{4,3,2,1}" : string opening ListSet type 'a t - val s2 = fromList [3,4,5,6]; val empty : ''a t val s2 = - : int t … lots of bindings omitted … val toString : (''a -> string) -> ''a t -> string - toList (union s1 s2); val it = [1,2,6,5,4,3] : int list

- isEmpty (empty); - toList (intersection s1 s2); val it = true : bool val it = [4,3] : int list-

- toList (difference s1 s2); - size (singleton 17); val it = [2,1] : int list val it = 1 : int - toList (difference s2 s1); - List.size (singleton 17); val it = [6,5] : int list

val it = 1 : int SML Modules and ADTS 27 SML Modules and ADTS 28

ListSetDups (soluLons in SML VM repo) FunSet (PS8) Specifying sets with predicates is fun! structure ListSetDups :> SET = struct Math: { x | x mod 3 = 0 }

type ''a t = ''a list SML: fn x => x mod 3 = 0 val empty = [] fun singleton x = [x] structure FunSet :> SET = fun insert x ys = x :: ys (* Allow dups *) struct type ''a t = ''a -> bool ... flesh out the rest in class ... val empty = fn _ => false end fun singleton x = fn y => x=y fun member x pred = pred x • Represent sets as unordered lists of elements, possibly fun fromPred pred = pred ... Flesh out the rest in PS7 ... containing duplicates. This simplifies some operaCons end and complicates others. Which? • When must duplicates be removed? • Which set operaCons are unimplementable in FunSet? • A removeDups helper funcCon is handy. • Is fromPred implementable in ListSet? SML Modules and ADTS 29 SML Modules and ADTS 30

OperaLonTreeSet (PS8) (delete 4 (difference (union (union (insert 1 empty) (insert 4 empty)) (union (insert 7 empty) (insert 4 empty))) (intersection (insert 1 empty) (union (insert 1 empty) (insert 6 empty)))))

SML Modules and ADTS 31