Abstract Classes
In Haskell, a class always extends an abstract class, which has to be declared before the concrete class.
import Prelude hiding ((.))
x.f = f x
-- show
data Shape = Circle {
radius :: Float
}
circ = Circle 12
main = do {
print(circ.radius);
}
-- /show
As we can see, the abstract class Shape
is declared by the keyword data
. So data Shape = Circle
"means" abstract class Shape {}
and class Circle extends Shape {}
combined. In FP lingo, we will refer to Shape
as the type of circ
rather than to circ
as an object that extends the abstract class Shape
.
Concrete Classes
All concrete classes that extend the same abstract class must be declared together.
import Prelude hiding ((.))
x.f = f x
-- show
data Shape = Circle { radius :: Float }
| Rectangle { width :: Float
, height :: Float }
circ = Circle 12
rect = Rectangle 16 9
main = do {
print(circ.radius^2 * pi);
print(rect.width * rect.height);
}
-- /show
We can observe, that Circle
and Rectangle
are not only the names of the concrete classes Circle {...}
and Rectangle { ... }
but also of their constructors Circle ...
and Rectangle ... ...
. In FP lingo, we will refer to Circle
and Rectangle
as the constructors of circ
and rect
rather than to circ
and rect
as instances of the concrete classes Circle
and Rectangle
.
(An abstract class that is extended by multiple concrete classes is called an algebraic data type in FP lingo, hence the keyword data
. In Scala, concrete classes that extend the same abstract class are called case classes.)
Because objects are immutable like values, they may be referred to as value objects in DDD. In FP lingo, objects are values, too.
Overloading
Methods are defined outside the class definition. Through their type they remain associated with their class, though.
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Data
import Prelude hiding ((.))
x.f = f x
getConstructor(x) = x.toConstr.show
data Shape = Circle { radius :: Float }
| Rectangle { width :: Float
, height :: Float } deriving (Data, Typeable)
circ = Circle 12
rect = Rectangle 16 9
-- show
areaCircle(this) = this.radius^2 * pi
areaRectangle(this) = this.width * this.height
main = do {
print(circ.areaCircle);
print(rect.areaRectangle);
}
-- /show
Methods are always associated with the abstract class, which means that that areaCircle
and areaRectangle
are in fact members of the abstract class Shape
. This allows for writing a single area
method associated with Shape
. All we have to do is to distinguish between the concrete classes Circle
and Rectangle
.
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Data
import Prelude hiding ((.))
x.f = f x
getConstructor(x) = x.toConstr.show
data Shape = Circle { radius :: Float }
| Rectangle { width :: Float
, height :: Float } deriving (Data, Typeable)
circ = Circle 12
rect = Rectangle 16 9
-- show
area(this) = if this.getConstructor == "Circle"
then this.radius^2 * pi
else if this.getConstructor == "Rectangle"
then this.width * this.height
else undefined
main = do {
print(circ.area);
print(rect.area);
}
-- /show
This looks quite cluttered and contains an awkward undefined
because Haskell has no naked if-then
expressions. Let's try a case switch instead of this nested if-then-else.
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Data
import Prelude hiding ((.))
x.f = f x
getConstructor(x) = x.toConstr.show
data Shape = Circle { radius :: Float }
| Rectangle { width :: Float
, height :: Float } deriving (Data, Typeable)
circ = Circle 12
rect = Rectangle 16 9
-- show
area(this) = case this.getConstructor of
"Circle" -> this.radius^2 * pi
"Rectangle" -> this.width * this.height
-- /show
main = do {
print(circ.area);
print(rect.area);
}
Constructor-based case switches are called pattern matching in FP lingo and they are supported out of the box, without getClass
ceremony:
import Prelude hiding ((.))
x.f = f x
data Shape = Circle { radius :: Float }
| Rectangle { width :: Float
, height :: Float }
circ = Circle 12
rect = Rectangle 16 9
-- show
area(this) = case this of
c@(Circle r) -> c.radius^2 * pi
r@(Rectangle w h) -> r.width * r.height
-- /show
main = do {
print(circ.area);
print(rect.area);
}
We're declaring inline r
, w
and h
, the arguments of Circle
and Rectangle
, but we don't use them. Instead, we use c
and r
to access radius
, width
and height
. Let's do some refactoring and access r
, w
and h
directly.
import Prelude hiding ((.))
x.f = f x
data Shape = Circle { radius :: Float }
| Rectangle { width :: Float
, height :: Float }
circ = Circle 12
rect = Rectangle 16 9
-- show
area(this) = case this of
Circle r -> r^2 * pi
Rectangle w h -> w * h
-- /show
main = do {
print(circ.area);
print(rect.area);
}
The most idiomatic way of pattern matching in Haskell is to overload a method for each pattern.
import Prelude hiding ((.))
x.f = f x
-- show
data Shape = Circle { radius :: Float }
| Rectangle { width :: Float
, height :: Float }
circ = Circle 12
rect = Rectangle 16 9
area(Circle r) = r^2 * pi
area(Rectangle w h) = w * h
main = do {
print(circ.area);
print(rect.area);
}
-- /show
In the next tutorial we shall see how to serialize an instance of Shape
by means of an interface.