H. Conrad Cunningham
25 September 2017
Acknowledgements: These slides accompany sections 2.6 “Using Data Abstraction” section 2.7 “Modular Design and Programming” from Chapter 2 “Basic Haskell Functional Programming” of “Introduction to Functional Programming Using Haskell”.
Advisory: The HTML version of this document may require use of a browser that supports the display of MathML. A good choice as of September 2017 is a recent version of Firefox from Mozilla.
Code: The Haskell modules for the Rational Arithmetic case study are in files RationalCore.hs
, Rational.hs
, and RationalDeferGCD.hs
.
Introduce modular development methods for
Describe how to use Haskell modules effectively with these methods
“a work assignment given to a programmer or group of programmers” (Parnas, 1978) – software engineering concept
also may be program unit defined with construct or convention – programming language concept
ideally language feature supports software engineering method
Enable programmers to understand the system by focusing on one module at a time (comprehensibility)
Shorten development time by minimizing required communication among groups (independent development)
Make system flexible by limiting number of modules affected by significant changes (changeability)
Forms cohesive unit of functionality separate from other modules
Hides a design decision (its secret) from other modules
Encapsulates aspect of system likely to change (its secret)
Aspects likely to change independently become secrets of separate modules
Aspects unlikely to change become interactions (connections) among modules
Interface:
“set of assumptions … each programmer needs to make about the other program … to demonstrate the correctness of his own program” (Britton, Parker, & Parnas, 1981)
includes function signatures (name, arguments, return)
includes constraints on environment and argument values (preconditions, postconditions, invariants)
Abstract interface:
does not change when one module implementation substituted for another
concentrates on module’s essential aspects and obscures incidental aspects that vary among implementations
Information hiding with abstract interfaces supports software reuse
Tradeoff conflicts among criteria: completeness vs. simplicity, reusability vs. simplicity, convenience vs. { consistency, simplicity, no redundancy, atomicity}
Tradeoff criteria against efficiency and functionality
Define in a separate file, enable separate compilation
Export public features (functions and data types)
Does not export private features (functions and data types)
Should represent good software engineering design
Rational numbers where
x
and y
integers
y
0
Data type Rat
Constructor makeRat :: Int -> Int -> Rat
where makeRat x y
creates
Selector functions numer, denom :: Rat -> Int
where
numer (makeRat x y) == x
denom (makeRat x y) == y
Assuming data abstraction (Rat
, makeRat
, numer
, denom
)
negRat :: Rat -> Rat
negRat x = makeRat (- numer x) (denom x)
addRat, subRat, mulRat, divRat :: Rat -> Rat -> Rat
addRat x y = makeRat (numer x * denom y + numer y * denom x)
(denom x * denom y) -- x + y
subRat x y = makeRat (numer x * denom y - numer y * denom x)
(denom x * denom y) -- x - y
mulRat x y = makeRat (numer x * numer y)
(denom x * denom y) -- x * y
divRat x y = makeRat (numer x * denom y)
(denom x * numer y) -- x / y
eqRat :: Rat -> Rat -> Bool
eqRat x y = (numer x) * (denom y) == (numer y) * (denom x)
Choose type synonym Rat
type Rat = (Int, Int)
(1,7)
, (-1,-7)
, (3,21)
, (168,1176)
all represent Choose canonical (normal) form (x,y)
that satisfies invariant
y > 0
, x
and y
are relatively prime(0,1)
Keeps integer values small to reduce overflow issues
Invariants assert what must hold for an “object” to be valid
First implement the constructor
makeRat :: Int -> Int -> Rat
makeRat x 0 = error ( "Cannot construct a rational number "
++ showRat (x,0) ++ "\n" )
makeRat 0 _ = (0,1)
makeRat x y = (x' `div` d, y' `div` d)
where x' = (signum' y) * x
y' = abs' y
d = gcd' x' y'
where
clause introduces x'
, y'
, and d
as local definitions within body of makeRat
rely on type inference for types of x'
, y'
and d
– are Int
because of makeRat
signature
signum' :: Int -> Int -- similar to signum in Prelude
signum' n | n == 0 = 0
| n > 0 = 1
| n < 0 = -1
abs' :: Int -> Int -- similar to abs in Prelude
abs' n | n >= 0 = n
| n < 0 = -n
gcd' :: Int -> Int -> Int -- greatest common divisor, gcd in Prelude
gcd' x y = gcd'' (abs' x) (abs' y)
where gcd'' x 0 = x
gcd'' x y = gcd'' y (x `rem` y)
showRat :: Rat -> String
showRat x = show (numer x) ++ "/" ++ show (denom x)
Now implement the selectors
numer, denom :: Rat -> Int
numer (x,_) = x
denom (_,y) = y
Package has groups of features
public rational arithmetic functions: negRat
, addRat
, subRat
, mulRat
, divRat
, eqRat
public type Rat
, constructor makeRat
, selectors numer
and denom
, string converter showRat
private utility functions used by second group, but just reimplementations of Prelude functions
RationalCore
Hide “secret” of Rational Number data representation
Reveal public interface to data abstraction: Rat
, makeRat
, numer
, denom
, showRat
Encapsulate implementations as Haskell module RationalCore
.
module RationalCore
(Rat, makeRat, numer, denom, showRat)
where
-- Rat, makeRat, numer, denom, showRat definitions
Include utility functions or use Prelude versions directly
Rational
(1)Hide “secret” of Rational Arithmetic operation implementation (and data representation)
Use core data abstraction through its public interface
Reveal public interface to Rational Arithmetic operations: negRat
, addRat
, subRat
, mulRat
, divRat
, eqRat
Also reveal public interface of core data abstraction: makeRat
, numer
, denom
, showRat
Rational
(2)Encapsulate implementations as Haskell Rational
module Rational
( Rat, makeRat, numer, denom, showRat, -- from RationalCore
negRat, addRat, subRat, mulRat, divRat, eqRat )
where
import RationalCore
-- negRat, addRat, subRat, mulRat, divRat, eqRat definitions
Offers potential robustness with respect to change
Allows multiple implementations of each module as long as interface is stable
Enables understanding of modules without understanding of internals of others
Costs some in terms of extra code and efficiency (but that probably does that matter)
Previous module RationalCore
:
constructor makeRat
creates pairs of relatively prime integers
selectors numer
and denom
just return stored integer
Alternative module RationalDeferGCD
:
constructor makeRat
just stores integers
selectors numer
and denom
returns relatively prime
Question: What are the advantages and disadvantages of the two data representations?
RationalDeferGCD
(1) module RationalDeferGCD
(Rat, makeRat, numer, denom, showRat)
where
type Rat = (Int,Int)
makeRat :: Int -> Int -> Rat
makeRat x 0 = error ( "Cannot construct a rational number "
++ showRat (x,0) ++ "\n" )
makeRat 0 y = (0,1)
makeRat x y = (x,y)
numer :: Rat -> Int
numer (x,y) = x' `div` d
where x' = (signum' y) * x
y' = abs' y
d = gcd' x' y'
denom :: Rat -> Int
denom (x,y) = y' `div` d
where x' = (signum' y) * x
y' = abs' y
d = gcd' x' y'
RationalDeferGCD
(2) showRat :: Rat -> String
showRat x = show (numer x) ++ "/" ++ show (denom x)
Invariants:
for rational number (x,y)
, y
0
and zero is represented by (0,1)
numer
and denom
satisfy the following, where y' > 0
, x'
and y'
are relatively prime, and
numer (makeRat x y) == x'
denom (makeRat x y) == y'
Module Rational
works withRationalCore
or RationalDeferGCD
Interface invariant for abstract module “RationalRep” – Haskell modules RationalCore
and RationalDeferGCD
For all integers
x
and nonzero integersy
,
numer (makeRat x y) == x'
denom (makeRat x y) == y'
where
y' > 0
,x'
andy'
are relatively prime,
and ifx'
= 0, theny'
= 1.
RationalCore
For all integers
x
and nonzero integersy
,
makeRat x y == (x',y')
where
y' > 0
,x'
andy'
are relatively prime,
and ifx'
= 0, theny'
= 1.
makeRat
does work, numer
and denom
trivialRationalDeferGCD
For all integers
x
and nonzero integersy
,
makeRat x y == (x,y)
makeRat
trivial, numer
and denom
do workChange alias Rat
to use Integer
Replace alias Rat
by a user-defined type Rat
Divide work differently between makeRat
, numer
, denom
Haskell 2010’s weak module system does not support multiple implementations well
GHC’s new mixin package system Backpack offers new possibilities
Adds a way to define interfaces to “abstract” modules
Module as unit of work in software engineering
information hiding
abstract interface
invariants
Criteria for good interfaces
Haskell module features
Rational numbers
The Haskell modules for the Rational Arithmetic case study are in files: