JoseJuan and MathematicalOrchid covered key points about hiding constructors while exposing types and interfaces, but there is another technique for managing certain kinds of invariants in Haskell: encode them in the type system. Algebraic data types do this to a certain degree on their own:
data Tree a = Tri (Tree a) a (Tree a) a (Tree a)
| Bin (Tree a) a (Tree a)
| Tip
is much more constrained than
newtype Tree a = Tree [a] [Tree a]
But you can go much further using nested types, phantom types, and GADTs. For example, Data.Sequence defines
newtype Elem a = Elem a
data Node a = Node2 !Int a a | Node3 !Int a a a
data Digit a = One a | Two a a | Three a a a | Four a a a a
data FingerTree a = Empty
| Single a
| Deep !Int (Digit a)
(FingerTree (Node a))
(Digit a)
newtype Seq a = Seq (FingerTree (Elem a))
Note that a deep FingerTree a contains a FingerTree (Node a). This is called a "nested" or "non-regular" type; it ensures that the 2-3 trees at each level are exactly one deeper than the ones at the previous level.
The same shape invariant could be maintained differently (but less efficiently, as it turns out) using phantom types and GADTs:
{-# LANGUAGE GADTs, DataKinds, KindSignatures #-}
data Nat = Z | S Nat
-- This is a GADT; n is a phantom type
data Tree23 (n::Nat) a where
Elem :: a -> Tree23 Z a
Node2 :: !Int -> Tree23 n a ->
Tree23 n a -> Tree23 (S n) a
Node3 :: !Int -> Tree23 n a -> Tree23 n a ->
Tree23 n a -> Tree23 (S n) a
-- n is again a phantom type
data Digit (n::Nat) a
= One (Tree23 n a)
| Two (Tree23 n a) (Tree23 n a)
| Three (Tree23 n a) (Tree23 n a) (Tree23 n a)
| Four (Tree23 n a) (Tree23 n a) (Tree23 n a) (Tree23 n a)
-- n is still a phantom type
data FingerTree (n::Nat) a
= Empty
| Single a
| Deep !Int (Digit n a) (FingerTree (S n) a) (Digit n a)
In this version, the level of the finger tree is "tracked" using a phantom type, and then the heights of the 2-3 trees are forced to match it using a GADT.