15

Consider:

{-# OPTIONS -fglasgow-exts #-}

data Second = Second
data Minute = Minute
data Hour = Hour

-- Look Ma', a phantom type!
data Time a = Time Int

instance Show (Time Second) where
  show (Time t) = show t ++ "sec" 

instance Show (Time Minute) where
  show (Time t) = show t ++ "min" 

instance Show (Time Hour) where
  show (Time t) = show t ++ "hrs" 

sec :: Int -> Time Second
sec t = Time t

minute :: Int -> Time Minute
minute t = Time t 

hour :: Int -> Time Hour
hour t = Time t 

class TimeAdder a b c | a b -> c where
  add :: Time a -> Time b -> Time c

instance TimeAdder Second Second Second where
  add (Time s1) (Time s2) = sec (s1 + s2)

instance TimeAdder Second Minute Second where
  add (Time s) (Time m) = sec (s + 60*m)

instance TimeAdder Second Hour Second where
  add (Time s) (Time h) = sec (s + 3600*h)

instance TimeAdder Minute Second Second where
  add (Time m) (Time s) = sec (60*m + s)

instance TimeAdder Minute Minute Minute where
  add (Time m1) (Time m2) = minute (m1 + m2)

instance TimeAdder Minute Hour Minute where
  add (Time m) (Time h) = minute (m + 60*h)

instance TimeAdder Hour Second Second where
  add (Time h) (Time s) = sec (3600*h + s)

instance TimeAdder Hour Minute Minute where
  add (Time h) (Time m) = minute (60*h + m)

instance TimeAdder Hour Hour Hour where
  add (Time h1) (Time h2) = hour (h1 + h2)

add (minute 5) (hour 2)
--125min

Although I'm quite thrilled that crazy stuff like this works, I wonder how the quadratic explosion of TimeAdder instances could be avoided.

Landei
  • 54,104
  • 13
  • 100
  • 195
  • 1
    Hours, minutes and seconds really aren't that good of a candidate for this kind of type safety, since why would you ever have a function that e.g. only accepts time in seconds? A better exercise for this kind of type-safety might be, for example, physical units. You could have let's say `Time`, `Mass`, `Length` etc. as phantom types and have type-safe calculations for speed, energy etc. This will help with the number of instances too since not all types are interchangeable like in your time example. – shang Dec 23 '11 at 07:02
  • @shang: This is correct. I should have mentioned that this is just a toy example for getting a better handle on type classes and phantom types. I understand that for a real world application with time units only hammar's first answer would be much more practical. – Landei Dec 23 '11 at 19:02

6 Answers6

13

Unless you have a good reason to, I would just skip the type classes and use a plain old ADT:

data Time = Hour Int | Minute Int | Second Int

instance Show Time where
  show (Hour x) = show x ++ "hrs"
  show (Minute x) = show x ++ "min"
  show (Second x) = show x ++ "sec"

add x y = fromSeconds (toSeconds x + toSeconds y)

toSeconds (Hour x) = 3600 * x
toSeconds (Minute x) = 60 * x
toSeconds (Second x) = x

fromSeconds x | mod x 3600 == 0 = Hour (div x 3600)
              | mod x 60 == 0 = Minute (div x 60)
              | otherwise = Second x

This has the advantage of being able to do certain simplifications that the type class approach can't, for example:

> add (Second 18) (Second 42)
1min
hammar
  • 138,522
  • 17
  • 304
  • 385
  • Good point! I'll definitely do it that way for real world applications. But I specifically tried to get a better understanding of phantom types in my example. – Landei Dec 22 '11 at 14:38
  • 5
    You can convert hammar's code back to type classes and it won't have the quadratic explosion, as it is using seconds as intermediate unit. – Sjoerd Visscher Dec 22 '11 at 15:32
  • 1
    @SjoerdVisscher Could you elaborate on that please? I don't see how you would get the correct phantom type in the return type. – dave4420 Dec 22 '11 at 18:23
  • @dave4420 Hmm, you are right, the return type would depend on the integer value, and that's not possible. – Sjoerd Visscher Dec 23 '11 at 12:10
9

You could do something like this, but it doesn't give you the functional dependency.

class TimeUnit a where
    toSeconds :: a -> Int
    fromSeconds :: Int -> a

instance TimeUnit (Time Second) where toSeconds = id; fromSeconds = id
instance TimeUnit (Time Minute) where toSeconds = (* 60); fromSeconds = (`quot` 60)

class TimeAdd a b c where
    add :: a -> b -> c

instance (TimeUnit a, TimeUnit b, TimeUnit c) => TimeAdd a b c where
    add a b = fromSeconds (toSeconds a + toSeconds b)
augustss
  • 22,884
  • 5
  • 56
  • 93
6

The way I would do this at the type level is to map the phantom types to type level natural numbers and use a "minimum" operation to find the correct return type and then let instance resolution do the job from there on.

I'll be using type families here, but it can probably be done with functional dependencies if you prefer those.

{-# LANGUAGE TypeFamilies, EmptyDataDecls, FlexibleInstances #-}

First, we'll need some type level naturals and a minimum operation.

data Zero
data Succ n

type family Min a b
type instance Min Zero a = Zero
type instance Min a Zero = Zero
type instance Min (Succ a) (Succ b) = Succ (Min a b)

Next, we'll define our phantom types and provide mappings to and from our type level naturals:

data Second
data Minute
data Hour

type family ToIndex a
type instance ToIndex Hour = Succ (Succ Zero)
type instance ToIndex Minute = Succ Zero
type instance ToIndex Second = Zero

type family FromIndex a
type instance FromIndex (Succ (Succ Zero)) = Hour
type instance FromIndex (Succ Zero) = Minute
type instance FromIndex Zero = Second

Next, the Time type and Show instances. These are the same as in your original code.

data Time a = Time Int

instance Show (Time Second) where
  show (Time t) = show t ++ "sec" 

instance Show (Time Minute) where
  show (Time t) = show t ++ "min" 

instance Show (Time Hour) where
  show (Time t) = show t ++ "hrs" 

sec :: Int -> Time Second
sec t = Time t

minute :: Int -> Time Minute
minute t = Time t 

hour :: Int -> Time Hour
hour t = Time t 

Just like in my ADT answer, we'll use seconds as the intermediate unit:

class Seconds a where
    toSeconds :: Time a -> Int
    fromSeconds :: Int -> Time a

instance Seconds Hour where
    toSeconds (Time x) = 3600 * x
    fromSeconds x = Time $ x `div` 3600

instance Seconds Minute where
    toSeconds (Time x) = 60 * x
    fromSeconds x = Time $ x `div` 60

instance Seconds Second where
    toSeconds (Time x) = x
    fromSeconds x = Time x

Now all that remains is to define the add function.

add :: (Seconds a, Seconds b, Seconds c,
       c ~ FromIndex (Min (ToIndex a) (ToIndex b)))
       => Time a -> Time b -> Time c
add x y = fromSeconds (toSeconds x + toSeconds y)

The magic happens in the type equality constraint, which makes sure that the correct return type is chosen.

This code can be used just like you wanted:

> add (minute 5) (hour 2)
125min

To add another unit, say Days, you only have to add instances for Show, FromIndex, ToIndex and Seconds, i.e. we've successfully avoided the quadratic explosion.

hammar
  • 138,522
  • 17
  • 304
  • 385
2

The first part cannot be done this way in Haskell 2010, because the restriction on instantiated types is that they be of the form

T t1 ... tn

where t1...tn are different type variables and that there is at most one instance pro type and class. In Frege, while the restrictions on the form of the type are lifted a bit, the crucial restriction remains at most one instance per class and type constructor. Here is a way to do the show-Part nevertheless:

module Test where

data Seconds = Seconds
data Minutes = Minutes
data Hours   = Hours

data Time u = Time Int

class TimeUnit u where
  verbose :: u -> String
  fromTime :: Time u -> u

instance TimeUnit Seconds where 
  verbose _  = "sec"
  fromTime _ = Seconds
instance TimeUnit Minutes where 
  verbose _   = "min"
  fromTime _  = Minutes
instance TimeUnit Hours   where 
  verbose _ = "hrs"
  fromTime _ = Hours

instance Show (TimeUnit u) => Time u where
  show (o@Time t) = t.show ++ verbose (fromTime o)

main _ = do
 println (Time 42 :: Time Seconds)
 println (Time 42 :: Time Minutes)
 println (Time 42 :: Time Hours)

The fromTime application forces the call site to construct an appropriate dictionary, so that a TimeUnit value can be made ex nihilo, or so it appears.

The same technique could be used to do the arithmetic between different Time types, by creating a factor that makes computations in the smallest unit possible.

Ingo
  • 36,037
  • 5
  • 53
  • 100
1

Taking hammar's suggestion 1 step further, I'd say for this particular example, just eliminate the type stuff altogether, and use smart constructors instead.

newtype Time = Sec Int

instance Show Time where
  show (Sec n) = h ++ " hrs " ++ m ++ " min " ++ s ++ " sec"
    where h = ...
          m = ...
          s = ...

sec :: Int -> Time
sec = Sec

min :: Int -> Time
min = sec . (*60)

hr  :: Int -> Time
hr  = min . (*60)

add (Sec n) (Sec m) = Sec (n+m)

Of course, that's no fun, since it has no phantom types. Fun exercise: make lenses for hr, min, sec.

Dan Burton
  • 53,238
  • 27
  • 117
  • 198
0

The instances are all fairly boilerplate. I'd say this is a case for Template Haskell (though I'll leave the explanation of how to do so to someone who's used it in anger).

dave4420
  • 46,404
  • 6
  • 118
  • 152