Imperative OOP Ceremony: Classes

As of March 2020, School of Haskell has been switched to read-only mode.

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.