[A little more about code design. Bryan O'Sullivan **20080107204323] { hunk ./en/ch10-binary.xml 406 - ordinary values a -> b and turns it into a + ordinary values a -> b and lifts it to become a hunk ./en/ch10-binary.xml 467 + In addition, we can't place any constraints on our type + definition. What does this mean? To illustrate, let's first + look at a normal &data; definition and its Functor + instance. + + &ValidFunctor.hs:Foo; + + When we define a new type, we can add a type constraint just + after the &data; keyword as follows. + + &ValidFunctor.hs:Bar; + + This says that we can only put a type a into a nFoo if a is a member of the Eq + typeclass. However, the constraint renders it impossible to + write a Functor instance for + Bar. + + &functor.ghci:invalid; + + + Constraints on type definitions are bad + + Adding a constraint to a type definition is never a good + idea. It has the effect of forcing you to add type + constraints to every function that will + operate on values of that type. Let's say that we need a + stack data structure that we want to be able to query to see + whether its elements obey some ordering. Here's a naive + definition of the data type. + + &TypeConstraint.hs:OrdStack; + + If we want to write a function that checks the stack to + see whether it is monotonic (i.e. either every element is + bigger than the element below it, or every element is + smaller), we'll obviously need an Ord constraint + to perform the pairwise comparisons. + + &TypeConstraint.hs:isMonotonic; + + However, because we wrote the type constraint on the type + definition, that constraint ends up infecting places where it + isn't needed at all: we need to add the Ord + constraint to push, which does not care + at all about the ordering of elements on the stack. + + &TypeConstraint.hs:push; + + Try removing that Ord constraint above, and + the definition of push will fail to + typecheck. + + This is why our attempt to write a Functor + instance for Bar failed earlier: it would have + required an Eq constraint to somehow get + retroactively added to the signature of + fmap. + + Now that we've tentatively established that putting a type + constraint on a type definition is a misfeature of Haskell, + what's a more sensible alternative? The answer is simply to + omit type constraints from type definitions, and instead place + them on the functions that need them. + + In this example, we can drop the Ord + constraints from OrdStack and + push. It needs to stay on + isMonotonic, which otherwise couldn't + call compare. We now have the + constraints where they actually matter. This has the further + benefit of making the type signatures better document the real + requirements of each function. + + Most Haskell container types follow this pattern. The + Map type in the Data.Map module + requires that its keys be ordered, but this constraint is + expressed on functions like insert, where + it's actually needed, and not on size, + where ordering isn't used. + + addfile ./examples/ch10/TypeConstraint.hs hunk ./examples/ch10/TypeConstraint.hs 1 +module TypeConstraint where + +{-- snippet OrdStack --} +data (Ord a) => OrdStack a = Bottom + | Item a (OrdStack a) + deriving (Show) +{-- /snippet OrdStack --} + +{-- snippet isMonotonic --} +isMonotonic :: (Ord a) => OrdStack a -> Bool +isMonotonic (Item a rest@(Item b _)) + | compare a b `elem` [GT, LT] = isMonotonic rest + | otherwise = False +isMonotonic _ = True +{-- /snippet isMonotonic --} + +{-- snippet push --} +push :: (Ord a) => a -> OrdStack a -> OrdStack a +push a s = Item a s +{-- /snippet push --} addfile ./examples/ch10/ValidFunctor.hs hunk ./examples/ch10/ValidFunctor.hs 1 +{-- snippet Foo --} +data Foo a = Foo a + +instance Functor Foo where + fmap f (Foo a) = Foo (f a) +{-- /snippet Foo --} + +{-- snippet Bar --} +data Eq a => Bar a = Bar a +{-- /snippet Bar --} + +{-- snippet Functor --} +instance Functor Bar where + fmap f (Bar a) = Bar (f a) +{-- /snippet Functor --} hunk ./examples/ch10/functor.ghci 58 +--# invalid +:load ValidFunctor + }