H. Conrad Cunningham
12 September 2018
Copyright (C) 2017, 2018, H. Conrad Cunningham
Acknowledgements: I originally created these slides in Fall 2017 to accompany what is now Chapter 7, Data Abstraction, in the 2018 version of the textbook Exploring Languages with Interpreters and Functional Programming.
Browser 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 2018 is a recent version of Firefox from Mozilla.
Code: The Haskell modules for the Rational Arithmetic case study are in files:
Illustrate use of data abstraction
Reinforce and extend the concepts of modular design and programming using Haskell modules
E.g. invariants
Procedural abstraction: separate properties of action from implementation details
Break complex actions into subprograms; develop program as sequence of calls
Data abstraction: separate properties of data from representation details
Identify key data representations; develop program around these and associated operations
Mathematical rational numbers where
x
and y
integers
y
0
Data type Rat
Constructor makeRat :: Int -> Int -> Rat
where makeRat x y
creates math
value
with
Selector functions numer, denom :: Rat -> Int
where:
if makeRat x y == r
in Haskell, then
in math
Constant zeroRat
represents rational number 0
Assuming data abstraction (Rat
, makeRat
, zeroRat
, 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)
subRat x y = makeRat (numer x * denom y - numer y * denom x)
(denom x * denom y)
mulRat x y = makeRat (numer x * numer y) (denom x * denom y)
divRat x y -- (2) (3)
| eqRat y zeroRat = error "Attempt to divide by 0"
| otherwise = makeRat (numer x * denom y)
(denom x * numer y)
eqRat :: Rat -> Rat -> Bool
eqRat x y = (numer x) * (denom y) == (numer y) * (denom x)
Choose type synonym Rat
(1,7)
,
(-1,-7)
,
(3,21)
,
(168,1176)
all represent
Choose canonical (normal) form (x,y)
that satisfies Rational
Representation Property (next slide)
(x,y)
(Int,Int)
y > 0
if x == 0
,
then y == 1
x
and y
are relatively
prime
rational number value is
Keeps integer magnitudes small β reduces Int
overflow
issues
Gives unique representation for zero
First implement constructor
makeRat :: Int -> Int -> Rat
makeRat x 0 = error ( "Cannot construct a rational number "
++ show x ++ "/0" )
makeRat 0 _ = zeroRat
makeRat x y = (x' `div` d, y' `div` d)
where x' = (signum' y) * x -- (1,2)
y' = abs' y
d = gcd' x' y'
where
clause introduces local definitions x'
, y'
, d
within makeRat
body
type inference for x'
, y'
, d
β all Int
because of
makeRat
signature
Precondition of makeRat x y
y /= 0
Postcondition of makeRat x y = (x',y')
(x',y')
satisfies the Rational Representation Property
Rational number value is
Together postconditions imply
signum' :: Int -> Int -- similar Prelude signum
signum' n | n == 0 = 0
| n > 0 = 1
| otherwise = -1
abs' :: Int -> Int -- similar Prelude abs
abs' n | n >= 0 = n
| otherwise = -n
gcd' :: Int -> Int -> Int -- similar Prelude gcd
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)
Preconditions for numer (x,y)
and denom (x,y)
Arguments
(x,y)
satisfy Rational Representation Property
Postconditions for numer (x,y) = x
and denom (x,y) = y
β no change to input
β no change to input
Package has groups of features:
public rational arithmetic functions: negRat
, addRat
, subRat
, mulRat
, divRat
, eqRat
public type Rat
, constant
zeroRat
, 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: Rat
, zeroRat
, makeRat
, numer
, denom
, showRat
β plus
pre/postconditions
Encapsulate implementations as Haskell module RationalCore
.
Include utility functions or use Prelude versions directly
Rational
(1)Hide secret of Rational Arithmetic operation implementation (and data representation)
Use RationalCore
data abstraction through its public interface
Reveal public interface to Rational Arithmetic operations: negRat
, addRat
, subRat
, mulRat
, divRat
, eqRat
β plus
pre/postconditions
Also reveal public interface of core data abstraction: makeRat
, numer
, denom
, showRat
β plus
pre/postconditions
Rational
(2)Encapsulate implementation as Haskell Rational1
Enables modules to be reused
Offers robustness with respect to change β data representation and arithmetic algorithms change independently
Allows multiple implementations of each module by keeping public interface stable
Enables understanding of one module without understanding of internal details of modules it uses
Costs some in extra code and execution efficiency β likely does not matter given benefits and compiler optimizations
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)RationalDeferGCD
(2) 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'
showRat :: Rat -> String
showRat x = show (numer x) ++ "/" ++ show (denom x)
-- signum', abs', gcd' as before
(x,y)
(Int,Int)
y /= 0
if x == 0
,
then y == 1
rational number value is
Compare: Allows y < 0
,
not relatively prime
RationalDeferGCD
Contracts (1)Precondition makeRat x y
y /= 0
Postcondition makeRat x y = (x',y')
(x',y')
satisfies the Deferred Representation Property
Rational number value is
RationalDeferGCD
Contracts (2)Precondition numer (x,y)
and denom(x,y)
(x,y)
satisfies Deferred Representation Property
Postconditions numer (x,y) = x'
and denom (x,y) = y
β no change to input
β no change to input
If makeRat x y == r
,
numer r = x'
,
and denom r = y'
,
then postconditions imply that
RationalDeferGCD
Hide βsecretβ of rational number data representation
Reveal public interface: Rat
, zeroRat
, makeRat
, numer
, denom
, showRat
β plus preconditions,
postconditions
Encapsulate implementations as Haskell module RationalDeferGCD
.
Include utility functions or use Prelude versions directly
RationalDeferGCD
RationalCore
and RationalDeferGCD
have same interface and functionality
are interchangeable
represent by βabstract moduleβ RationalRep
(not
Haskell 2010 concept)
Rational1
and Rational2
RationalRep
importInvariants assert what must hold for an βobjectβ to be valid
Preconditions of all public operations except constructors
Postconditions of all public operations except destructors
For any valid Haskell rational number r
:
r
Rat
denom r > 0
If
numer r == 0
, thendenom r == 1
numer r
anddenom r
are relatively
Rational number value is
Abstract interface precondition makeRat x y
y /= 0
Abstract interface postcondition makeRat x y = r
r
satisfies the RationalRep Interface Invariant
Rational number
r
βs value is
Abstract interface precondition numer r
and denom r
r
satisfies the RationalRep Interface Invariant
Abstract interface postcondition numer r = x'
and denom r = y'
Rational number value equals rational number value of
r
Rational number value equals rational number value of
r
RationalCore
Implementation Inv.For any valid Haskell rational number r
:
r == (x,y)
for some(x,y)
Rat
y > 0
if
x == 0
, theny == 1
x
andy
are relatively prime
Rational number value is
Implies interface invariant given RationalCore
implementation
RationalDeferGCD
Implementation Inv.For any valid Haskell rational number r
:
r == (x,y)
for some(x,y)
Rat
y /= 0
if
x == 0
, theny == 1
Rational number value is
Implies interface invariant given RationalCore
implementation
Change 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
Backpack adds a way to define interfaces to βabstractβ modules
Chapter 12 discusses testing of package β test against abstract interface and contracts
Module RationalRep
TestRatRepCore.hs
for RationalCore
TestRatRepDefer.hs
for RationalDeferGCD
Module Rational
TestRational1.hs
for Rational
using
RationalCore
.
TestRational2.hs
for Rational
using
RationalDeferGCD
.
Define in a separate file β enables separate compilation, reuse
Export public features (functions and data types)
Do not export private features (functions and data types)
Use good software engineering design
Data abstraction as design, implementation, and testing technique
Haskell modules
Rational numbers
RationalCore.hs
module
Rational1.hs
module
RationalDeferGCD.hs
module
Rational2.hs
module
Testing listed on earlier slide