Presentation is loading. Please wait.

Presentation is loading. Please wait.

Programming LanguagesSection 41 Programming Languages Section 4. SML Remaining Topics Xiaojuan Cai Spring 2015.

Similar presentations


Presentation on theme: "Programming LanguagesSection 41 Programming Languages Section 4. SML Remaining Topics Xiaojuan Cai Spring 2015."— Presentation transcript:

1 Programming LanguagesSection 41 Programming Languages Section 4. SML Remaining Topics Xiaojuan Cai Spring 2015

2 Programming LanguagesSection 42 Remaining Topics Type Inference Mutual Recursion Module System Equivalence – optional Lambda calculus – in separate slides

3 Programming LanguagesSection 43 Some concepts Static type-checking v.s. Dynamic type-checking Static: ML, Java, C,.. Dynamic: Racket, Ruby, Python,... Implicit type v.s. Explicit type Implicit: ML, Racket,... Explicit: C, Java,... Type-checking v.s. Type inference Type-checking: check type consistency Type inference: infer types for implicit type language

4 Programming LanguagesSection 44 ML type inference Determine types of bindings in order (Except for mutual recursion) For each val or fun binding: Analyze definition for all necessary facts (constraints) Example: If see x > 0, then x must have type int Type error if no way for all facts to hold Afterward, use type variables (e.g., 'a ) for any unconstrained types Example: An unused argument can have any type

5 Programming LanguagesSection 45 Very simple example val x = 42 (* val x : int *) fun f (y, z, w) = if y (* y must be bool *) then z + x (* z must be int *) else 0 (* both branches have same type *) (* f must return an int f must take a bool * int * ANYTHING so val f : bool * int * 'a -> int *)

6 Programming LanguagesSection 46 Relation to Polymorphism Central feature of ML type inference: it can infer types with type variables But remember there are two orthogonal concepts Languages can have type inference without type variables Languages can have type variables without type inference

7 Programming LanguagesSection 47 More examples Collect all the facts needed for type- checking These facts constrain the type of the function See the code file and/or the reading notes

8 Programming LanguagesSection 48 A local optimum Despite the value restriction, ML type inference is elegant and fairly easy to understand More difficult without polymorphism What type should length-of-list have? More difficult with subtyping val (y,z) = x constrains x to have at least two fields, not exactly two fields Depending on details, languages can support this, but types often more difficult to infer and understand Will study subtyping later, but not with type inference

9 Programming LanguagesSection 49 Where are we? Type Inference Mutual Recursion Module System Equivalence

10 Programming LanguagesSection 410 Mutual Recursion Allow f to call g and g to call f Useful? Yes. Idiom we will show: implementing state machines

11 Programming LanguagesSection 411 New language features Mutually recursive functions (the and keyword) Similarly, mutually recursive datatype bindings Everything in “mutual recursion bundle” type- checked together and can refer to each other fun f1 p1 = e1 and f2 p2 = e2 and f3 p3 = e3 datatype t1 = … and t2 = … and t3 = …

12 Programming LanguagesSection 412 State-machine example Each “state of the computation” is a function “State transition” is “call another function” with “rest of input” Generalizes to any finite-state-machine example fun state1 input_left = … and state2 input_left = … and …

13 Programming LanguagesSection 413 Work-around Suppose we did not have support for mutually recursive functions Can have the “later” function pass itself to the “earlier” one Yet another higher-order function idiom fun earlier (f,x) = … f y … … (* no need to be nearby *) fun later x = … earlier(later,y) …

14 Programming LanguagesSection 414 Where are we? Type Inference Mutual Recursion Module System Equivalence

15 Programming LanguagesSection 415 Modules Inside a module Can have any kind of binding (val, datatype, exception,...) Outside a module, Can refer to modules’ bindings via ModuleName.bindingName structure MyModule = struct bindings end

16 Programming LanguagesSection 416 Example structure MyMathLib = struct fun fact x = if x=0 then 1 else x * fact(x-1) val half_pi = Math.pi / 2 fun doubler x = x * 2 end

17 Programming LanguagesSection 417 Open Can use open ModuleName to get “direct” access to a module’s bindings Never necessary; just a convenience; often bad style Often better to create local val-bindings for just the bindings you use a lot, e.g., val map = List.map

18 Programming LanguagesSection 418 Namespace management So far, this is just namespace management Giving a hierarchy to names to avoid shadowing Allows different modules to reuse names, e.g., map Very important, but not very interesting

19 Programming LanguagesSection 419 Signatures signature MATHLIB = sig val fact : int -> int val half_pi : real val doubler : int -> int end structure MyMathLib :> MATHLIB = struct fun fact x = … val half_pi = Math.pi / 2.0 fun doubler x = x * 2 end

20 Programming LanguagesSection 420 In general Signatures Can include variables, types, datatypes, and exceptions defined in module Ascribing a signature to a module Module will not type-check unless it matches the signature, meaning it has all the bindings at the right types structure MyModule :> SIGNAME = struct bindings end signature SIGNAME = sig types-for-bindings end

21 Programming LanguagesSection 421 Hiding things Real value of signatures is to to hide bindings and type definitions Hiding implementation details is the most important strategy for writing correct, robust, reusable software

22 Programming LanguagesSection 422 Hiding with functions Defining helper functions locally is also powerful Can change/remove functions later and know it affects no other code Would be convenient to have “private” top-level functions too So two functions could easily share a helper function ML does this via signatures that omit bindings… fun double x = x*2 fun double x = x+x val y = 2 fun double x = x*y

23 Programming LanguagesSection 423 Example Outside the module, MyMathLib.doubler is simply unbound signature MATHLIB = sig val fact : int -> int val half_pi : real end structure MyMathLib :> MATHLIB = struct fun fact x = … val half_pi = Math.pi / 2.0 fun doubler x = x * 2 end

24 Programming LanguagesSection 424 A larger example Now consider a module that defines an Abstract Data Type (ADT) Our example: rational numbers supporting add and toString structure Rational1 = struct datatype rational = Whole of int | Frac of int*int exception BadFrac (*internal functions gcd and reduce not on slide*) fun make_frac (x,y) = … fun add (r1,r2) = … fun toString r = … end

25 Programming LanguagesSection 425 Library spec and invariants Properties [externally visible guarantees, up to library writer] Disallow denominators of 0 Return strings in reduced form (“4” not “4/1”, “3/2” not “9/6”) No infinite loops or exceptions Invariants [part of the implementation, not the module’s spec] All denominators are greater than 0 All rational values returned from functions are reduced

26 Programming LanguagesSection 426 More on invariants Our code maintains the invariants and relies on them Maintain: make_frac disallows 0 denominator, removes negative denominator, and reduces result add assumes invariants on inputs, calls reduce if needed Rely: gcd does not work with negative arguments, but no denominator can be negative add uses math properties to avoid calling reduce toString assumes its argument is already reduced

27 Programming LanguagesSection 427 A first signature With what we know so far, this signature makes sense: gcd and reduce not visible outside the module signature RATIONAL_A = sig datatype rational = Whole of int | Frac of int*int exception BadFrac val make_frac : int * int -> rational val add : rational * rational -> rational val toString : rational -> string end structure Rational1 :> RATIONAL_A = …

28 Programming LanguagesSection 428 The problem Any of these would lead to exceptions, infinite loops, or wrong results, which is why the module’s code would never return them Rational1.Frac(1,0) Rational1.Frac(3,~2) Rational1.Frac(9,6) signature RATIONAL_A = sig datatype rational = Whole of int | Frac of int*int …

29 Programming LanguagesSection 429 So hide more Key idea: An ADT must hide the concrete type definition Alas, this attempt doesn’t work because the signature now uses a type rational that is not known to exist: signature RATIONAL_WRONG = sig exception BadFrac val make_frac : int * int -> rational val add : rational * rational -> rational val toString : rational -> string end structure Rational1 :> RATIONAL_WRONG = …

30 Programming LanguagesSection 430 Abstract types So ML has a feature for exactly this situation: In a signature: type foo means the type exists, but clients do not know its definition signature RATIONAL_B = sig type rational exception BadFrac val make_frac : int * int -> rational val add : rational * rational -> rational val toString : rational -> string end structure Rational1 :> RATIONAL_B = …

31 Programming LanguagesSection 431 This works! Only way to make first rational is Rational1.make_frac After that can use only Rational1.make_frac, Rational1.add, and Rational1.toString Hides constructors and patterns – don’t even know whether or not Rational1.rational is a datatype signature RATIONAL_B = sig type rational exception BadFrac val make_frac : int * int -> rational val add : rational * rational -> rational val toString : rational -> string end

32 Programming LanguagesSection 432 Two key restrictions So we have two powerful ways to use signatures for hiding: 1. Deny bindings exist (val-bindings, fun- bindings, constructors) 2. Make types abstract (so clients cannot create values of them or access their pieces directly)

33 Programming LanguagesSection 433 A cute twist In our example, exposing the Whole constructor is no problem Still hiding the rest of the datatype Still does not allow using Whole as a pattern signature RATIONAL_C = 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

34 Programming LanguagesSection 434 Signature matching structure Foo :> BAR is allowed if: Every non-abstract type in BAR is provided in Foo, as specified Every abstract type in BAR is provided in Foo in some way Can be a datatype or a type synonym Every val-binding in BAR is provided in Foo, possibly with a more general and/or less abstract internal type Every exception in BAR is provided in Foo

35 Programming LanguagesSection 435 Equivalent implementations Example (see code file): structure Rational2 does not keep rationals in reduced form, instead reducing them “at last moment” in toString Also make gcd and reduce local functions Not equivalent under RATIONAL_A Rational1.toString(Rational1.Frac(9,6)) = "9/6" Rational2.toString(Rational2.Frac(9,6)) = "3/2” Equivalent under RATIONAL_B or RATIONAL_C Different invariants, but same properties Essential that type rational is abstract

36 Programming LanguagesSection 436 More interesting example Given a signature with an abstract type, different structures can: Have that signature But implement the abstract type differently Such structures might or might not be equivalent Example (see code): type rational = int * int Does not have signature RATIONAL_A Equivalent to both previous examples under RATIONAL_B or RATIONAL_C

37 Programming LanguagesSection 437 More interesting example structure Rational3 = struct type rational = int * int exception BadFrac fun make_frac (x,y) = … fun Whole i = (i,1) (* needed for RATIONAL_C *) fun add ((a,b)(c,d)) = (a*d+b*c,b*d) fun toString r = … (* reduce at last minute *) end

38 Programming LanguagesSection 438 Some interesting things Internally make_frac has type int * int -> int * int, but externally int * int -> rational Could give type rational -> rational in signature, but this is awful: makes entire module unusable – why? Internally Whole has type 'a -> 'a * int but externally int -> rational This matches because we can specialize 'a to int and then abstract int * int to rational

39 Programming LanguagesSection 439 Can’t mix-and-match module bindings Modules with the same signatures still define different types So things like this do not type-check: Rational1.toString(Rational2.make_frac(9,6)) Rational3.toString(Rational2.make_frac(9,6))

40 Programming LanguagesSection 440 Where are we? Type Inference Mutual Recursion Module System Equivalence

41 Programming LanguagesSection 441 Equivalence Must reason about “are these equivalent” all the time Code maintenance: Can I simplify this code? Backward compatibility: Can I add new features without changing how any old features work? Optimization: Can I make this code faster? Abstraction: Can an external client tell I made this change?

42 Programming LanguagesSection 442 A definition Two functions are equivalent if they have the same “observable behavior” in any context: Given equivalent arguments, they: Produce equivalent results Have the same (non-)termination behavior Mutate (non-local) memory in the same way Do the same input/output Raise the same exceptions

43 Programming LanguagesSection 443 Example Since looking up variables in ML has no side effects, these two functions are equivalent: But these next two are not equivalent in general: it depends on what is passed for f Are equivalent if argument for f has no side-effects Example: g ((fn i => print "hi" ; i), 7) fun f x = x + x val y = 2 fun f x = y * x fun g (f,x) = (f x) + (f x) val y = 2 fun g (f,x) = y * (f x)

44 Programming LanguagesSection 444 Another example Again: pure functions make more things equivalent Example: g divides by 0 and h mutates a top-level reference Example: g writes to a reference that h reads from fun f x = let val y = g x val z = h x in (y,z) end fun f x = let val z = h x val y = g x in (y,z) end

45 Programming LanguagesSection 445 Syntactic sugar Using or not using syntactic sugar is always equivalent By definition, else not syntactic sugar But be careful about evaluation order fun f x = if x then g x else false fun f x = x andalso g x fun f x = if g x then x else false fun f x = x andalso g x

46 Programming LanguagesSection 446 Standard equivalences 1. Consistently rename bound variables and uses But notice you can’t use a variable name already used in the function body to refer to something else val y = 14 fun f x = x+y+x val y = 14 fun f z = z+y+z val y = 14 fun f x = x+y+x val y = 14 fun f y = y+y+y fun f x = let val y = 3 in x+y end fun f y = let val y = 3 in y+y end

47 Programming LanguagesSection 447 Standard equivalences 2. Use a helper function or do not But notice you need to be careful about environments val y = 14 fun f x = x+y+x fun g z = (f z)+z val y = 14 fun g z = (z+y+z)+z val y = 14 fun f x = x+y+x val y = 7 fun g z = (f z)+z val y = 14 val y = 7 fun g z = (z+y+z)+z

48 Programming LanguagesSection 448 Standard equivalences 3. Unnecessary function wrapping But notice that if you compute the function to call and that computation has side-effects, you have to be careful fun f x = x+x fun g y = f y fun f x = x+x val g = f fun f x = x+x fun h () = (print "hi"; f) fun g y = (h()) y fun f x = x+x fun h () = (print "hi"; f) val g = (h())

49 Programming LanguagesSection 449 One more If we ignore types, then ML let-bindings can be syntactic sugar for calling an anonymous function: These both evaluate e1 to v1, then evaluate e2 in an environment extended to map x to v1 So exactly the same evaluation of expressions and result But in ML, there is a type-system difference: x on the left can have a polymorphic type, but not on the right Can always go from right to left If x need not be polymorphic, can go from left to right let val x = e1 in e2 end (fn x => e2) e1

50 Programming LanguagesSection 450 What about performance? According to our definition of equivalence, these two functions are equivalent, but we learned one is awful (Actually we studied this before pattern-matching) fun max xs = case xs of [] => raise Empty | x::[] => x | x::xs’ => if x > max xs’ then x else max xs’ fun max xs = case xs of [] => raise Empty | x::[] => x | x::xs’ => let val y = max xs’ in if x > y then x else y end

51 Programming LanguagesSection 451 Different definitions for different jobs PL Equivalence: given same inputs, same outputs and effects Good: Lets us replace bad max with good max Bad: Ignores performance in the extreme Asymptotic equivalence: Ignore constant factors Good: Focus on the algorithm and efficiency for large inputs Bad: Ignores “four times faster” Systems equivalence: Account for constant overheads, performance tune Good: Faster means different and better Bad: Beware overtuning on “wrong” (e.g., small) inputs; definition does not let you “swap in a different algorithm”

52 Programming LanguagesSection 452 Conclusion Type Inference Mutual Recursion Module System Equivalence


Download ppt "Programming LanguagesSection 41 Programming Languages Section 4. SML Remaining Topics Xiaojuan Cai Spring 2015."

Similar presentations


Ads by Google