Reputation: 5901
In my Data
data User = User { uRank :: Int, uProgress :: Int }
I want to limit uRank
to a list of values [-1, 1, 3], for example.
How do I do this?
Upvotes: 9
Views: 2866
Reputation: 64740
Liquid Haskell, which adds refinement types on top of Haskell, can do this. See the correct section of the tutorial here.
First you define your data type as normal.
module User where
data User = User { uRank :: Int
, uProgress :: Int
}
Next we define your restriction as a refinement type. Liquid Haskell users comment annotations with syntax {-@ blah @-}
. We'll define your odd restriction of -1, 1, 3
first with the RestrictedInt
type:
{-@ type RestrictedInt = {v : Int | v == -1 || v == 1 || v == 3} @-}
That is, the RestrictedInt
is an Int
that is either -1
, 1
, or 3
. Notice you could easily write ranges and other restrictions, it doesn't have to be some mindless enumeration of legitimate values.
Now we re-define your data type in our refinement language using this restricted int type:
{-@ data User = User { uRank :: RestrictedInt
, uProgress :: Int }
@-}
This is similar to your definition but with the restriction type instead of just Int
. We could have inlined the restriction instead of having a type alias but then your User
data type would be pretty unreadable.
You can define good values and the liquid
tool will not complain:
goodValue :: User
goodValue = User 1 12
But bad values result in an error:
badValue :: User
badValue = User 10 12
$ liquid so.hs
(or your editor's integration, if you have that setup) produces:
**** RESULT: UNSAFE ************************************************************
so.hs:16:12-18: Error: Liquid Type Mismatch
16 | badValue = User 10 12
^^^^^^^
Inferred type
VV : {VV : GHC.Types.Int | VV == ?a}
not a subtype of Required type
VV : {VV : GHC.Types.Int | VV == (-1)
|| (VV == 1
|| VV == 3)}
In Context
?a := {?a : GHC.Types.Int | ?a == (10 : int)}
Upvotes: 5
Reputation: 2697
Defining a small sum type is the best answer for this specific question. You can also use newtype
with smart constructors to achieve this effect.
newtype Rank = UnsafeMkRank { unRank :: Int }
mkRank :: Int -> Maybe Rank
mkRank i
| i `elem` [-1, 1, 3] = Just (UnsafeMkRank i)
| otherwise = Nothing
Now, provided you only use the safe mkRank
constructor, you can assume that your Rank
values have the Int
values you want.
Upvotes: 10
Reputation: 530823
For something so small, you should define a precise type:
data Rank = RankNegOne | RankOne | RankThree -- There are probably better names
data User = User { uRank :: Rank, uProgress :: Int }
Haskell doesn't force you to encode everything as a subset of the integers; take advantage of it!
For larger subsets where this would be unwieldy, you are looking for a dependent type, which (until recently?) is not supported by Haskell directly.
Upvotes: 8