klozovin
klozovin

Reputation: 2373

Type class instance with more restrictive signature

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

Answers (1)

user2297560
user2297560

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

Related Questions