CS 641 Lecture -*- Outline -*- Lecture adapted from Mads Tofte's "Four lectures on Standard ML" Edinburgh ECS-LFCS-89-73 * Programming with ML modules introduction to ML's modules and method for programming ** Motivation: programmin in the large core language is interactive, leads to bottom up development (building values, types...) need modules for programming in the large, separate compilation can use documentation standards, conventions (fragile) better to provide direct support in language many of same constructs needed for prog-i-t-large e.g., if M2 uses M1.f, then actual M1 must provide f single language (e.g., Pebble) for both small and large scale prog. types, modules are values problem: no static type checking statified language (ML) types are not values, modules language contains core but not vice versa static type checking problem: flexibility, expressiveness solution: can perform certain operations on structures i.e., have functors from structures to structures limits: functors cannot take functors as arguments, or produce them as results ML goes a long way to meeting needs. Imagine building a symbol table for a parser. sketch of design by writing down signatures ** Signatures OTable is opaque in that it hides many details ---------------- signature OTable = sig type table exception Lookup val lookup: table * Sym.sym -> Val.value val update: table * Sym.sym * Val.value -> table end; ---------------- imagine structures Sym and Val that describe symbols and values of symbols (types sym and val) note: Sym.sym is an example of a long identifier (a long type constructor) Sym and Val are free in OTble implement as a hash table require Sym to provide a hash function (Sym.sym -> int) assume another structure IntMap (arrays) for storing the table with polymorphic type IntMap.map ---------------- signature TTable = sig datatype table = TBL of (Sym.sym * Val.value)list IntMap.map exception Lookup val lookup: table * Sym.sym -> Val.value val update: table * Sym.sym * Val.value -> table end; ---------------- ** Structures implementation of a signature ---------------- structure SymTbl: TTable = struct datatype table = TBL of (Sym.sym * Val.value)list IntMap.map exception Lookup fun find(sym,[]) = raise Lookup | find(sym,(sym',v)::rest) = if sym = sym' then v else find(sym,rest); fun lookup(TBL map, s) = let val n = Sym.hash(s) in let val l = IntMap.apply(map,n) in find(s,l) end end handle IntMap.NotFound => raise Lookup (* ... *) end; ---------------- *** signature constraints structure SymTbl: OTable = ... ensures that find is hidden, also the constructor TBL may want to view the same structure through more than one signature (e.g., testing, different clients) can bind the same structure to more than one structure identifier ------------ structure SmallTbl: OTable = SymTbl ------------ *** sharing **** dynamic semantics each struct ... end evaluation yields a different environment just one lookup function, shared between SymTbl and SmallTbl SymTbl.lookup = SmallTbl.lookup **** static semantics two sets of names created SymTbl.lookup and SmallTbl.lookup have same type ? is this necessary ?? (motivation: it's the same value...) so SymTbl.table and SmallTbl.table are also shared ** Functors OTable and SymTbl both have free identifiers problem: can't compile them. solution: abstract away the free identifiers ---------------- functor SymTblFct( structure IntMap: IntMapSig structure Val: ValSig structure Sym: SymSig): sig type table exception Lookup val lookup: table * Sym.sym -> Val.value val update: table * Sym.sym * Val.value -> table end = struct datatype table = TBL of (Sym.sym * Val.value)list IntMap.map exception Lookup fun find(sym,[]) = raise Lookup | find(sym,(sym',v)::rest) = if sym = sym' then v else find(sym,rest); fun lookup(TBL map, s) = let val n = Sym.hash(s) in let val l = IntMap.apply(map,n) in find(s,l) end in find(s,l) end handle IntMap.NotFound => raise Lookup (* ... *) end; ---------------- exercise: declare the signatures IntMapSig, ValSig, SymSig, and finish the body of SymTblFct after have defined structures, can get a symbol table: ------------- structure MyTbl = SymTblFct(structure IntMap = FastIntMap structure Val = Data structure Sym = Identifier) ------------- functor body is evaluated each time the functor is applied What is the signature of MyTbl? not the result signature of SymTblFct (refers to Val, Sym) suppose Identifier.sym = string and Data.value = real ** Substructures The result signature of SymTblFct depends on actual argument types (signature of SymTblFct is a dependent type) How to declare ParseFct to can be applied to any symbol table? explict substructures for depenents ------------ signature SymTblSig = sig structure Val: ValSig structure Sym: SymSig type table exception Lookup val lookup: table * Sym.sym -> Val.value val update: table * Sym.sym * Val.value -> table end; ------------ note: assumes that ValSig and SymSig are declared elsewhere structures can contain other structures (substructures) ---------------- functor SymTblFct( structure IntMap: IntMapSig structure Val': ValSig structure Sym': SymSig): SymTblSig = struct structure Val = Val' structure Sym = Sym' datatype table = TBL of (Sym.sym * Val.value)list IntMap.map exception Lookup (* ... *) end; ---------------- ** Sharing signature for lexical analyzers have to include substructure Sym at least before defining a Sym structure makes dependency explicit ------------- signature LExSig = sig structure Sym : SymSig val getsym : unit -> Sym.sym end; ------------- Parse functor what's wrong? ------------- functor ParseFct(structure SymTbl: SymTblSig structure Lex: LexSig) = struct (* ...*) let val next = Lex.getsym() in SymTbl.update(table, next, "declared") end end; ------------- problem is the let expression has a type error (see it?) type of getsym() is Lex.Sym.sym second argument of update has type SymTbl.Sym.sym can't assume these are the same sharing specification solves this problem can specify sharing of structures or types (not values, exceptions) -------------- functor ParseFct(structure SymTbl: SymTblSig structure Lex: LexSig sharing SymTbl.sym = Lex.Sym and type SymTbl.Val.value = string) = struct (* ...*) let val next = Lex.getsym() in SymTbl.update(table, next, "declared") end end; -------------- ** Building the system *** always use functors for writing code because can separately (export and import to files) functors exercise: write nullary Sym and Val functors *** linking is top-level structure delcarations and functor application ------------------ structure Val = ValFct() structure Sym = SymFct() structure TTable = SymTblFct(structure IntMap = IntMapFct() structure Val = Val strucutre Sym = Sym) structure Lex = LexFct(Sym) structure Parser = ParseFct(structure SymTbl = TTable structure Lex = Lex) ------------------ exercise: why are Val and Sym declared first, instead of being instantiated at each point of use? ** Separate compilation Our version of Standard ML has essentially no separate compilation like Harper's "export" primitive Even so, can separately compile modules as above, although results won't outlast a session. However, one can save the entire session (snapshot). file organization: functor result signature symtbl.sml, symtbl.sig, symbol.sml, symbol.sig etc. ------------ (* format of a typical .sml file with functor definition *) use "signatures.sml"; functor ParseFct( ... ------------ put use of all signatures in a single file, because order is important, and don't want to record it several times ------------ (* signatures.sml file *) use "symbol.sig"; use "value.sig"; use "symtbl.sig"; use "lex.sig"; use "parse.sig"; ----------- ** Good Style keep signatures as small as possible use the "include" specification to enrich a signature instead of always adding on to existing signatures ------------- signature BigTbl = sig include SmallTbl datatype DebugInfo = (* ... *) val printInfo : unit -> unit end; ------------- ** Bad Style signature decls can contain free structure and type identifiers structure decls can contain free identifiers of any kind why? so can write ------------- structure Parser = struct structure Lex = Lex structure MyPervasives = MyPervasives structure ErrorReports = ErrorReports structure PrintFcns = PrintFcns structure Table = Table structure BigTable = BigTable structure Aux = Aux fun f(...) = ... Table.lookup ... end; ------------- problem: no errors if missed some structures, how is this avoided by using functors? have to find declaration of Table to see what it's signature is how is this avoided by using functors? does Table.lookup depend on other structures? do other structures depend on it? misuse of "open" long identifiers help figure out where code is from ------------- structure Parser = struct structure Lex = Lex open MyPervasives ErrorReports PrintFcns Table BigTable Aux fun f(...) = ... lookup ... end; -------------