Reputation: 2373
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...
Upvotes: 2
Views: 138
Reputation: 2983
Consider what this class declaration is stating:
class Slide a where
slide :: Coordinate x -> a -> Coordinate x
for any type x
, an instance of Slide
promises that given a Coordinate x
and an a
, it will give you back a Coordinate x
. Right there is your problem. You don't want any x
all the time.
I think the easiest way to achieve what you want is with a second type class parameter for the coordinate type:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
class Slide x a where
slide :: Coordinate x -> a -> Coordinate x
instance Slide X Int64 where
slide = slideByInt
instance Slide Y Int64 where
slide = slideByInt
instance Slide X (Coordinate X) where
slide = slideByCoord
instance Slide Y (Coordinate Y) where
slide = slideByCoord
The last two instances can actually be replaced with this more general instance:
{-# LANGUAGE TypeFamilies #-}
instance (a ~ b) => Slide a (Coordinate b) where
slide = slideByCoord
For what it's worth, I like to avoid using typeclasses in this manner. I don't think the immediate convenience of overloaded functions is worth the boilerplate and long-term maintenance burden. But that's just my opinion.
Upvotes: 5