Let's say I'm writing a data type to represent a coordinate in a cartesian coordinate system. I'd like to define functions on that data type and use Haskell type checking to prevent mixing up numbers that lie on x axis with the numbers on the y axis.
Here's the data type definition, with a phantom type that tracks the coordinate axis and two functions to construct the values:
data X
data Y
newtype Coordinate axis = Coordinate Int64 deriving (Show)
newX :: Int64 -> Coordinate X
newX = Coordinate
newY :: Int64 -> Coordinate Y
newY = Coordinate
Let's define a sliding function that slides the coordinate, either by Int value or another Coordinate value. In the first case the coordinate should keep its axis and in the second case both arguments should have the same axis:
slideByInt :: Coordinate a -> Int64 -> Coordinate a
slideByInt (Coordinate x) y = Coordinate $ x + y
slideByCoord :: Coordinate a -> Coordinate a -> Coordinate a
slideByCoord (Coordinate x) (Coordinate y) = Coordinate (x + y)
This all works great and it prevents me from confusing X and Y axis in functions that manipulate Coordinates.
My question is: how would I wrap slideByInt
and slideByCoord
functionality behind a class, so that I can have just with the slide
function. This compiles:
class Slide a where
slide :: Coordinate x -> a -> Coordinate x
instance Slide Int64 where
slide (Coordinate x) y = Coordinate (x + y)
instance Slide (Coordinate x) where
slide (Coordinate x) (Coordinate y) = Coordinate (x + y)
but it's not as type safe as the standalone functions: slide (newX 1) (newY 1)
should not type check! How would one go about fixing this, in a sense, how can I make the instance for two Coordinates less permissive than it is?
I've tried with a bunch of extensions (InstanceSigs, FunctionalDependencies, type constraints...) but nothing compiles and it's hard to tell if that's the wrong way completely or I just have to tweak my code a little bit.
Thanks...