Reputation: 806
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
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
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
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