GeneralBecos
GeneralBecos

Reputation: 2556

Creating custom data types with constraints

I'm trying to create a custom data type. As an example

data Time  = Second Int deriving (Show)

However, this is too limiting (we could say later need milliseconds). I would like to instead define something like this:

data Time  = Second Num deriving (Show)

This doesn't compile because Num has kind * -> ghc-prim-0.4.0.0:GHC.Prim.Constraint

How do I setup Time such that Second may contain any Num?

Upvotes: 1

Views: 704

Answers (2)

ely
ely

Reputation: 77404

One of the best examples of why this might not be so desirable is found here at the Wikibooks section on Classes and Types. They say:

Type constraints in data declarations are less useful than it might seem at first. Consider:

data (Num a) => Foo a = F1 a | F2 a String

Here, Foo is a type with two constructors, both taking an argument of a type a which must be in Num. However, the (Num a) => constraint is only effective for the F1 and F2 constructors, and not for other functions involving Foo. Therefore, in the following example...

fooSquared :: (Num a) => Foo a -> Foo a
fooSquared (F1 x)   = F1 (x * x)
fooSquared (F2 x s) = F2 (x * x) s

... even though the constructors ensure a will be some type in Num we can't avoid duplicating the constraint in the signature of fooSquared

This suggests that a reasonable option for you is to just create Time with a generic parameter, and then later ensure that the module functions that operate on Time data always have the necessary constraint for Num.

It won't be so much of a worry that someone goes off and foolishly makes Time String or something -- if they do, then none of the provided module functions are going to be helpful for them, so it doesn't matter so much.

There are also options to look up with GADTs, the {-# LANGUAGE GeneralizedNewtypeDeriving #-} pragma, and the {-# LANGUAGE DatatypeContexts #-} pragma. But usually these start to rope in unnecessary degrees of extra complexity, especially if you're a Haskell novice like me.

Upvotes: 3

fjarri
fjarri

Reputation: 9726

There is a deprecated feature called Datatype Contexts that allows you to do that:

{-# LANGUAGE DatatypeContexts #-}
data Num a => Time a = Second a deriving (Show)
t = Second (5 :: Int)
main = print t

This executes on GHC 7.8.3 (sorry, don't have 7.10 to check), but warns you about the deprecation:

t.hs:1:14: Warning:
    -XDatatypeContexts is deprecated: It was widely considered a
misfeature, and has been removed from the Haskell language.
Second 5

A non-deprecated way to do it is to use Generalized Algebraic Datatypes (GADTs) (and you'll need standalone deriving as well):

{-# LANGUAGE GADTs, StandaloneDeriving #-}
data Time a where
    Second :: Num a => a -> Time a
deriving instance Show a => Show (Time a)
t = Second (5 :: Int)
main = print t

If you try to create a variable with something non-Num, you'll get a compilation error:

t = Second "a"

t.hs:12:5:
    No instance for (Num [Char]) arising from a use of ‘Second’
    In the expression: Second "a"
    In an equation for ‘t’: t = Second "a"

Upvotes: 2

Related Questions