Reputation: 179
I'm curious if it's possible to write something like this (pseudo code) in Haskell:
data Clock = Clock {hour :: (0 <= Int <= 24), minutes :: (0 <= Int <= 60)}
to make impossible (at the type level) to create something like this:
let a = Clock 34 236
Or should one use a kind of Clock builder which will check and create a valid Clock time?
Upvotes: 1
Views: 276
Reputation: 10695
One simple way to do this is to use smart encapsulation and smart constructors:
module HoursAndMinutes (Hours, mkHours, getHours, Minutes, mkMinutes, getMinutes) where
newtype Hours = UnsafeHours Int
mkHours :: Int -> Maybe Hours
mkHours n | 0 <= n && n < 24 = Just (UnsafeHours n)
| otherwise = Nothing
getHours :: Hours -> Int
getHours (UnsafeHours n) = n
newtype Minutes = UnsafeMinutes Int
mkMinutes :: Int -> Maybe Minutes
mkMinutes n | 0 <= n && n < 60 = Just (UnsafeMinutes n)
| otherwise = Nothing
getMinutes :: Minutes -> Int
getMinutes (UnsafeMinutes n) = n
Now in your other code if you import HoursAndMinutes
then you cannot construct invalid Hours
or Minutes
.
data Clock = Clock {hours :: Hours, minutes :: Minutes}
Then this will result in Nothing
:
Clock <$> mkHours 34 <*> mkMinutes 236
You can use the built-in predicates from refined
package to simplify this approach slightly:
data Clock = Clock
{ hours :: Refined (FromTo 0 23) Int
, minutes :: Refined (FromTo 0 59) Int
}
Then you can construct one with:
Clock <$> refine 34 <*> refine 236
Which will be Left (RefineSomeException ...)
.
The disadvantage is still that the checks are at runtime. You can get compile time checks by using Template Haskell:
Clock $$(refineTH 34) $$(refineTH 236)
This will throw an exception at compile time.
You can make this more convenient by using Liquid Haskell as Willem van Onsem suggests in his comment:
module Clock where
data Clock = Clock { hours :: Int, minutes :: Int } deriving Show
{-@ data Clock = Clock
{ hours :: {h:Int | 0 <= h && h < 24}
, minutes :: {m:Int | 0 <= m && m < 60}
} @-}
goodClock :: Clock
goodClock = Clock 16 49
badClock :: Clock
badClock = Clock 34 236 -- this gives an error
Upvotes: 7
Reputation: 120751
The concrete example is best expressed with types from the modular-arithmetic package:
{-# LANGUAGE TypeOperators, TypeFamilies, DataKinds #-}
import Data.Modular
data Clock = Clock {hour :: Int/24, minutes :: Int/60}
Note that the upper bounds are exclusive here, as you should have probably had in your code as well.
Modular arithmetic is a special case of a quotient type, which is in some ways more practical than constrained types. But Haskell has no general, direct support for either. As Willem Van Onsen mentioned, Liquid Haskell adds such functionality. Can't say much about that, but I've heard multiple times that it works surprisingly well.
Upvotes: 7