Peter
Peter

Reputation: 806

Expand Haskell datatypes

Is it possible to expand data types with new values?

E.g.: The following compiles:

data Axes2D = X | Y
data Axes3D = Axes2D | Z

But, the following:

data Axes2D = X | Y deriving (Show, Eq)
data Axes3D = Axes2D | Z deriving (Show, Eq)

type Point2D = (Int, Int)
type Point3D = (Int, Int, Int)

move_along_axis_2D :: Point2D -> Axes2D -> Int -> Point2D
move_along_axis_2D (x, y) axis move | axis == X = (x + move, y)
                                    | otherwise = (x, y + move)

move_along_axis_3D :: Point3D -> Axes3D -> Int -> Point3D
move_along_axis_3D (x, y, z) axis move | axis == X = (x + move, y, z)
                                       | axis == y = (x, y + move, z)
                                       | otherwise = (x, y, z + move) 

gives the following compiling error (move_along_axis_3D commented out doesn't give errors):

Prelude> :l expandTypes_test.hs 
[1 of 1] Compiling Main             ( expandTypes_test.hs, interpreted )

expandTypes_test.hs:12:50:
    Couldn't match expected type `Axes3D' with actual type `Axes2D'
    In the second argument of `(==)', namely `X'
    In the expression: axis == X
    In a stmt of a pattern guard for
                 an equation for `move_along_axis_3D':
          axis == X
Failed, modules loaded: none.

So is it possible to make X and Y of type Axes2D as well of type Axes3D? If it is possible: what am I doing wrong? Else: why is it not possible?

Upvotes: 2

Views: 374

Answers (3)

dr0b3rts
dr0b3rts

Reputation: 21

You can do it with Generalized Algebraic Data Types. We can create a generic (GADT) type with data constructors that have type constraints. Then we can define specialized types (type aliases) that specifies the full type and thus limiting which constructors are allowed.

{-# LANGUAGE GADTs #-}

data Zero
data Succ a

data Axis a where
  X :: Axis (Succ a)
  Y :: Axis (Succ (Succ a))
  Z :: Axis (Succ (Succ (Succ a)))

type Axis2D = Axis (Succ (Succ Zero))
type Axis3D = Axis (Succ (Succ (Succ Zero)))

Now, you are guaranteed to only have X and Y passed into a function that is defined to take an argument of Axis2D. The constructor Z fails to match the type of Axis2D.

Unfortunately, GADTs do not support automatic deriving, so you will need to provide your own instances, such as:

instance Show (Axis a) where
  show X = "X"
  show Y = "Y"
  show Z = "Z"
instance Eq (Axis a) where
  X == X = True
  Y == Y = True
  Z == Z = True
  _ == _ = False

Upvotes: 2

ehird
ehird

Reputation: 40787

Along with what Daniel Fischer said, to expand on why this is not possible: the problems with the kind of subtyping you want run deeper than just naming ambiguity; they make type inference a lot more difficult in general. I think Scala's type inference is a lot more restricted and local than Haskell's for this reason.

However, you can model this kind of thing with the type-class system:

class (Eq t) => HasAxes2D t where
  axisX :: t
  axisY :: t

class (HasAxes2D t) => HasAxes3D t where
  axisZ :: t

data Axes2D = X | Y deriving (Eq, Show)
data Axes3D = TwoD Axes2D | Z deriving (Eq, Show)

instance HasAxes2D Axes2D where
  axisX = X
  axisY = Y

instance HasAxes2D Axes3D where
  axisX = TwoD X
  axisY = TwoD Y

instance HasAxes3D Axes3D where
  axisZ = Z

You can then use guards to "pattern-match" on these values:

displayAxis :: (HasAxes2D t) => t -> String
displayAxis axis
  | axis == axisX = "X"
  | axis == axisY = "Y"
  | otherwise = "Unknown"

This has many of the same drawbacks as subtyping would have: uses of axisX, axisY and axisZ will have a tendency to become ambiguous, requiring type annotations that defeat the point of the exercise. It's also a fair bit uglier to write type signatures with these type-class constraints, compared to using concrete types.

There's another downside: with the concrete types, when you write a function taking an Axes2D, once you handle X and Y you know that you've covered all possible values. With the type-class solution, there's nothing stopping you from passing Z to a function expecting an instance of HasAxes2D. What you really want is for the relation to go the other way around, so that you could pass X and Y to functions expecting a 3D axis, but couldn't pass Z to functions expecting a 2D axis. I don't think there's any way to model that correctly with Haskell's type-class system.

This technique is occasionally useful — for instance, binding an OOP library like a GUI toolkit to Haskell — but generally, it's more natural to use concrete types and explicitly favour what in OOP terms would be called composition over inheritance, i.e. explicitly wrapping "subtypes" in a constructor. It's not generally much of a bother to handle the constructor wrapping/unwrapping, and it's more flexible besides.

Upvotes: 10

Daniel Fischer
Daniel Fischer

Reputation: 183873

It is not possible. Note that in

data Axes2D = X | Y
data Axes3D = Axes2D | Z

the Axes2D in the Axes3D type is a value constructor taking no arguments, so Axes3D has two constructors, Axes2D and Z.

Different types cannot have value constructors with the same name (in the same scope) because that would make type inference impossible. What would

foo X = True
foo _ = False

have as a type? (It's a bit different with parametric types, all Maybe a have value constructors with the same name, and that works. But that's because Maybe takes a type parameter, and the names are shared only among types constructed with the same (unary) type constructor. It doesn't work for nullary type constructors.)

Upvotes: 8

Related Questions