type - nullary constructor haskell

Input checks in Haskell data constructors (2)

How can I add input checks to Haskell data constructors? Let's say I have

import Data.Time.Calendar

data SchedulePeriod = SchedulePeriod { startDate :: Day
    , endDate :: Day
    , accrualStart :: Day
    , accrualEnd :: Day
    , resetDate :: Day
    , paymentDate :: Day
    , fraction :: Double }
    deriving (Show)

and I want to impose a constraint startDate < endDate. Is there a way to do it without creating an abstract data type?

Accept ehird's answer. I'm just writing this so I can explain the smart destructors I mentioned in a comment and I can't fit the explanation in a comment.

Let's say that you have the type:

data T x y z = A | B x | C y z

ehird already explained how to abstract away the constructor, which is just to provide "smart" constructors. Like you mentioned, this requires hiding the constructors and then you can't use them to pattern match. However, you can solve this using a "smart" destructor, which is equivalent to pattern-matching against every possible constructor.

To explain this, let's first start with how we'd write a function of type T if the constructors were exposed:

myFunction :: T x y z -> d
myFunction t = case t of
    A     -> f1
    B x   -> f2 x
    C y z -> f3 y z

We know from the function's type signature that the types of f1, f2, and f3 must be:

f1 :: d
f2 :: x -> d
f3 :: y -> z -> d

So if we were to generalize myFunction to be a smart destructor, we just pass f1, f2, and f3 as parameters to it:

smartDestructor :: d -> (x -> d) -> (y -> z -> d) -> t -> d
smartDestructor f1 f2 f3 t = case t of
    A     -> f1
    B x   -> f2 x
    C y z -> f3 y z

So if you export smartDestructor, then people can basically pattern-match against your type without needing access to the constructors.

If you've ever used the maybe or either functions, before, then you've used a smart destructor, although in those cases the constructors are not hidden, so they are mainly provided as convenience functions:

maybe :: b -> (a -> b) -> Maybe a -> b
maybe f1 f2 m = case m of
    Nothing -> f1
    Just  a -> f2 x

either :: (a -> c) -> (b -> c) -> Either a b -> c
either f1 f2 e = case e of
    Left  a -> f1 a
    Right b -> f2 b

In your case, the purpose of the smart destructor is just so you can hide the constructors and not expose the constructors.

The standard way is to use a smart constructor that checks the precondition before creating the value, and to not export the real constructor it uses. Of course, this is creating an abstract data type, as you said.

The only way to achieve this without a smart constructor would be really evil type-system hackery (and you wouldn't be able to use the standard Day type).