Я бы сделал это на уровне типов, чтобы сопоставить фантомные типы с натуральными числами уровня типов и использовать операцию «минимум», чтобы найти правильный тип возвращаемого значения, а затем позволить разрешению экземпляра выполнять свою работу с этого момента.
Я буду использовать семейства типов здесь, но, вероятно, это можно сделать с функциональными зависимостями, если вы их предпочитаете.
{-# LANGUAGE TypeFamilies, EmptyDataDecls, FlexibleInstances #-}
Во-первых, нам понадобятся некоторые натуральные числа уровня и минимумоперация.
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)
Далее мы определим наши фантомные типы и предоставим сопоставления с натуральными объектами уровня типов:
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
Далее тип Time
и Show
экземпляры.Они такие же, как в вашем исходном коде.
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
Так же, как в моем ответе ADT, мы будем использовать секунды в качестве промежуточной единицы:
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
Теперь все, что осталось, этоопределить функцию add
.
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)
Волшебство происходит в ограничении на равенство типов, которое гарантирует, что выбран правильный тип возвращаемого значения.
Этот код можно использовать так же, какВы хотели:
> add (minute 5) (hour 2)
125min
Чтобы добавить другой юнит, скажем, Days
, вам нужно только добавить экземпляры для Show
, FromIndex
, ToIndex
и Seconds
, т.е. мы успешноизбежал квадратичного взрыва.