OO in Haskell and Go versus Haskell

Martin Sulzmann

Overview

We show how to model OO in Haskell by making using of a combination of

As a running example we consider geometric objects.

We also draw a comparison among Go and Haskell.

Geometric objects

Recall

class Shape a where
   area :: a -> Int

data Square = Square Int

data Rectangle = Rec { len :: Int, width :: Int }


instance Shape Square where
  area (Square x)    = x * x

instance Shape Rectangle where
  area r    = len r * width r

class (Show a, Shape a) => ShapeExt a where
   scale :: a -> Int -> a


instance ShapeExt Square where
  scale (Square x) s = Square (x * s)

instance ShapeExt Rectangle where
  scale r s = Rec { len = len r * s, width = width r * s }

instance Show Square where
  show (Square x) = "square(" ++ show x ++ ")"

instance Show Rectangle where
  show  (Rec {len = l, width = w }) = "rectangle(" ++ show l ++ "," ++ show w ++ ")"

Can we store two geometric objects in a list?

The following will not type check.

r = Rec { width = 2, len = 3 }
s = Square 4

ls = [r, s]

Here’s the type error message.

    • Couldn't match expected type ‘Rectangle’
                  with actual type ‘Square’
    • In the expression: s
      In the expression: [r, s]
      In an equation for ‘ls’: ls = [r, s]

Abstract representation for geometric objects

data GeoShape = forall a. ShapeExt a => MkGeoShape a

The above data type declaration makes use of existential data types in combination with a type class. Why existential? There seems to be a universal (for all) quantor in the above definition? Yes, we universally quantify over a but this type is not part of the return type. Hence, the following logical equivalence

forall a. (I implies J)        =     (exists a. I) implies J

where J does not refer to a

which justifies the name existential data type.

In more detail, the constructor MkGeoShape has the following type.

MkGeoShape :: ShapeExt a => a -> GeoShape

The argument of the constructor is any value of type a that is an instance of the ShapeExt type class.

The following type checks.

s1 :: GeoShape
s1 = MkGeoShape r

s2 :: GeoShape
s2 = MkGeoShape s

shapes = [s1, s2, MkGeoShape r2]

By embedding square and rectangle into the new type GeoShape, we can store both in a list.

What can do with elements of this list? We can apply all the operations that are available for geometric objects by lifting them to GeoShape.

instance Show GeoShape where
    show (MkGeoShape x)    = show x

instance Shape GeoShape where
    area (MkGeoShape x)    = area x

instance ShapeExt GeoShape where
    scale (MkGeoShape x) s =  MkGeoShape (scale x s)

Here is an example.

ex2 = putStrLn (unlines (["Sum of the area of"] ++
                         map show shapes ++
                         [show (sum (map area shapes))]))

Go versus Haskell

Go interfaces are existential types constrained by a type class

The Go interface

type shape interface {
    area() int
}

can be represented in Haskell as follows

data GShape = forall a. Shape a => MkShape a

instance Shape GShape where
  area (MkShape x) = area x

Structural subtyping corresponds to interface constructors

The Go example

var r rectangle = rectangle{1, 2}
var s square = square{3}

x := sumArea(r, s)  // applied on (rectangle, square)
fmt.Printf("%d \n", x)

represented in Haskell

-- Square <= GShape
sqToGShape :: Square -> GShape
sqToGShape s = MkShape s

-- Rectangle <= GShape
recToGShape :: Rectangle -> GShape
recToGShape r = MkShape r


sumArea :: GShape -> GShape -> Int
sumArea x y = area x + area y


sumEx = let r = Rec 1 2
            s = Square 3
        in sumArea (recToGShape r) (sqToGShape s)

The only difference is that coercions such as recToGShape need to be inserted explicitly in Haskell.

Complete source code

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}

class Shape a where
   area :: a -> Int

data Square = Square Int

data Rectangle = Rec { len :: Int, width :: Int }


instance Shape Square where
  area (Square x)    = x * x

instance Shape Rectangle where
  area r    = len r * width r

class (Show a, Shape a) => ShapeExt a where
   scale :: a -> Int -> a


instance ShapeExt Square where
  scale (Square x) s = Square (x * s)

instance ShapeExt Rectangle where
  scale r s = Rec { len = len r * s, width = width r * s }

instance Show Square where
  show (Square x) = "square(" ++ show x ++ ")"

instance Show Rectangle where
  show  (Rec {len = l, width = w }) = "rectangle(" ++ show l ++ "," ++ show w ++ ")"


data GeoShape = forall a. ShapeExt a => MkGeoShape a

-- Equivalent to the above using GADT notation
data GeoShape2 where
   MkGeoShape2 :: ShapeExt a => a -> GeoShape2


instance Show GeoShape where
    show (MkGeoShape x)    = show x

instance Shape GeoShape where
    area (MkGeoShape x)    = area x

instance ShapeExt GeoShape where
    scale (MkGeoShape x) s =  MkGeoShape (scale x s)



r = Rec { width = 2, len = 3 }
s = Square 4

-- yields a type error
-- ls = [r, s]


r2 = scale r 3

s1 :: GeoShape
s1 = MkGeoShape r

s2 :: GeoShape
s2 = MkGeoShape s

shapes = [s1, s2, MkGeoShape r2]


ex2 = putStrLn (unlines (["Sum of the area of"] ++
                         map show shapes ++
                         [show (sum (map area shapes))]))


-- Go versus Haskell


data GShape = forall a. Shape a => MkShape a

instance Shape GShape where
  area (MkShape x) = area x

-- Square <= GShape
sqToGShape :: Square -> GShape
sqToGShape s = MkShape s

-- Rectangle <= GShape
recToGShape :: Rectangle -> GShape
recToGShape r = MkShape r


sumArea :: GShape -> GShape -> Int
sumArea x y = area x + area y


sumEx = let r = Rec 1 2
            s = Square 3
        in sumArea (recToGShape r) (sqToGShape s)