== Lecture Summary == - We briefly mentioned a few new examples of functorial library design: - in PS4 you will create a variety of implementations of a ListLike interface, and use functors to implement generic tests and module extensions - This same pattern is useful for other abstract data types that can be implemented with a variety of structures. - in PS5 you will write a general framework for distributed computation, and a variety of applications that use that framework; by implementing the framework functorially, you can reuse the distributed computation functor with different application modules. - This same pattern is useful for many kinds of plugin architectures - We discussed abstraction functions, which describe how to interpret a concrete value as a member of an abstract mathematical set. - example: "we interpret the pair (n,d) as the rational number n/d" - example: "we interpret the list [a;b;c] as the mathematical set {a,b,c}" - We discussed representation invariants (rep. invariants), which describe which concrete values are 'valid' - example: "the pair (n,d) is valid if d <> 0" - example: "the list [a1; a2; ...; an] is valid if the ai are all distinct" - We discussed the use of hiding type signatures from module types to allow the implementer of a module to maintain rep. invariants: - if we write module Rational : Fraction with type t = int * int then a user can create "bad" Rational.t's by simply writing (1,0). This invalid number will cause Rational to violate its specs - if instead we write module Rational : Fraction then a user knows there is a type Rational.t, but not that it is int * int; if they attempt to use Rational functions with the value (1,0), they will get a type error (expected "Rational.t" but got "int * int") - this is a tradeoff; the former is more convenient (it's nice to write down (1,0), but the latter is safer (the code that can create Rational.ts is limited to the implementation of the Rational module). - We discussed the fact that (many) functional specs can be implemented as functions: - example: "let plus_is_commutative a b = a + b = b + a" - example: "let times_is_associative a b c = (a * b) * c = a * (b * c)" - example: "let union_contains_both a s1 s2 = if contains s1 a || contains s2 a then contains (union s1 s2) a else not (contains (union s1 s2))" - we could imagine writing a general driver that plugs a variety of values into these specifications, reporting any pair of (numbers/sets) that don't satisfy the specs. See "Note on testing" below for additional information about implementing such a test driver. - We also talked about implementing a test function for a rep invariant. Unlike the specification tests, the repOk test is internal to the module implementation (since the outside world may not even know about the concrete data type at all!). - example: (in Rationals) "let repOk (n,d) = d <> zero" - example: (in Sets) "let repOk s = has_no_duplicates s" - We then introduced a different form of repOk function that we can use more conveniently: we would like to easily throw "repOk" checks into our code, so we modify the repOk function to return its input if it is valid and throw an exception otherwise: - example (in Rationals) let repOk (n,d) = if d = zero then failwith "rep. invariant violated!" else (n,d) - this lets us easily use the repOk function everywhere we return a value of type t: let (+) (n1,d1) (n2,d2) = repOk M.(n1*d2 + n2*d1, d1*d2) let (~-) (n,d) = repOk M.(-n,d) - by aggressively including repOk, we force an exception to occur as soon as the invariant is violated, instead of at some arbitrary time in the future when the "bad" Rational.t causes a bug == Additional reading == - the lecture slides and notes from last semester are a good additional resource (TODO: link) == Provided code == - The attached source code expands on our running number library example by adding specification tests for the Core interfaces, and documenting the abstraction functions and rep invariants for the concrete implementations - This lecture focuses on the following sections of the code: AddableSpec, MultipliableSpec, and so on in algebra.mli The implementations of the Spec functors in algebra.ml The documentation of the abstraction functions and rep invariants in the modules in impls.ml The TEST_MODULE statements in impls.ml The other new code is the test driver (which we imagined in lecture but did not examine in detail) See "Note on testing" below. - Note: the previous versions of the code are still useful to look at; some of the comments that were specific to those lectures have been removed. - Note on testing: it turns out that the cs3110 testing framework integrates nicely with generic specification tests like the ones we described in this lecture. This is supported by the TEST_MODULE = primitive, which runs all of the TESTs in the specified . In our case, we want to run the tests contained in the module Spec(Core). However, the person who writes the spec doesn't know what the interesting corner cases are for any given implementation; in fact they do not know what the concrete data types are. Therefore, I have extended the Spec modules to take a second module argument which contains a single value: the list of examples. Therefore the TEST_MODULE statements apply the Spec modules to two arguments: the module being tested, and an inline module listing the examples. These examples are then combined and passed to the specification functions. The helper functions that combine the values together and produce nice error messages are contained in the TestDriver module (in testDriver.ml/i). - Payoff: after running "cs3110 test impls.ml" we can look at inline_tests.log and note that 94 separate tests have been run. In fact, each of these tests are run on between 5 and 100 inputs (since each one runs on all pairs or triples of example inputs). Thousands of test runs with tens of lines of code? Yes please! We also see that the failures have predicted the problems with the code that uses numbers. If we filter out just the name of the module test that fails from the list of exceptions, we see which modules don't meet the specs: cs3110 test impls.ml 2>&1 | grep TEST_MOD in TEST_MODULE at file "impls.ml", line 30, characters 0-95: Ints in TEST_MODULE at file "impls.ml", line 30, characters 0-95: Ints in TEST_MODULE at file "impls.ml", line 30, characters 0-95: Ints in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 69, characters 0-120: Floats in TEST_MODULE at file "impls.ml", line 224, characters 0-89: Rationals in TEST_MODULE at file "impls.ml", line 224, characters 0-89: Rationals in TEST_MODULE at file "impls.ml", line 236, characters 0-153: Vec2OfFloat in TEST_MODULE at file "impls.ml", line 236, characters 0-153: Vec2OfFloat We see that Ints, Floats, Rationals (which are built from Ints), and Vec2OfFloats all fail at least one test; these are exactly the modules for which the whole application crashes. BigInts, BigRationals, and Vec2OfBigRats pass all tests, and are the modules that work. If we inspected the test report messages more closely, we would see exactly what properties fail on exactly which inputs. Note that I also found at least one bug in last lecture's code from these tests. I've updated it in this release but haven't changed the old release; feel free to find them yourself!