views:

299

answers:

4

Firstly, this question isn't 100% specific to Haskell, feel free to comment on the general design of typeclasses, interfaces and types.

I'm reading LYAH - creating types and typeclasses The following is the passage that I'm looking for more information on:

Data (Ord k) => Map k v = ...  

However, it's a very strong convention in Haskell to never add typeclass constraints in data declarations. Why? Well, because we don't benefit a lot, but we end up writing more class constraints, even when we don't need them. If we put or don't put the Ord k constraint in the data declaration for Map k v, we're going to have to put the constraint into functions that assume the keys in a map can be ordered. But if we don't put the constraint in the data declaration, we don't have to put (Ord k) => in the type declarations of functions that don't care whether the keys can be ordered or not. An example of such a function is toList, that just takes a mapping and converts it to an associative list. Its type signature is toList :: Map k a -> [(k, a)]. If Map k v had a type constraint in its data declaration, the type for toList would have to be toList :: (Ord k) => Map k a -> [(k, a)], even though the function doesn't do any comparing of keys by order.

This at first, seems logical -- but isn't there an upside to having the typeclass attached to the type? If the typeclass is the behavior of the type, then why should the behavior be defined by the use of the type (through functions), and not the type itself? I assume there is some meta-programming that could make use of it, and it is certainly nice and descriptive code-documentation. Conversely, would this be a good idea in other languages? Would it be ideal to specify the interface the object should conform to on the method, such that if the method isn't used by the caller the object doesn't have to conform to the interface? Moreover, why can Haskell not infer that a function using type Foo, has to pull in the typeclass constraints identified in type Foo's declaration? Is there a pragma to enable this?

The first time I read it, it conjured a "that's a hack (or workaround) response". On second read with some thought, it sounded clever. On third read, drawing a compairison to the OO world, it sounded like a hack again.

So here I am.

+3  A: 

The main reason to avoid typeclass constraints in data declarations is that they accomplish absolutely nothing; in fact, I believe GHC refers to such a class context as the "stupid context". The reason for this is that the class dictionary is not carried around with the values of the data type, so you have to add it to every function operating on the values anyway.

As a way of "forcing" the typeclass constraint on functions operating on the data type, it also doesn't really accomplish anything; functions should generally be as polymorphic as possible, so why force the constraint onto things that don't need it?

EDIT: I previously wrote "I'm not sure if it would be possible to change the semantics of ADTs in order to carry the dictionary around with the values." It seems like this is the whole point of GADTs; for example, you can do:

data Foo a where { Foo :: (Eq a) => a -> Foo a }
eqfoo :: Foo t -> Foo t -> Bool
eqfoo (Foo a) (Foo b) = a == b

Notice that the type of eqfoo does not need the Eq constraint, as it is "carried" by the Foo data type itself.

mithrandi
+7  A: 

Perhaps Map k v wasn't the best example to illustrate the point. Given the definition of Map, even though there are some functions that won't need the (Ord k) constraint, there is no possible way to construct a Map without it.

One often finds that a type is quite usable with the sub-set of functions that work on without some particular constraint, even when you envisioned the constraint as an obvious aspect of your original design. In such cases, having left the constraint off the type declaration leaves it more flexible.

For example, Data.List contains plenty of functions that require (Eq a), but of course lists are perfectly useful without that constraint.

MtnViewMark
"there is no possible way to construct a Map without it." -- this is precisely the reason I've always felt that things like Ord contexts on functions that _consume_ maps would be redundant, and also the reason for GADT matches introducing contexts. The fact that i have a `Map k v` is already proof enough that k is ordered. If all I'm doing is extracting things from the map, for example, why should I need an Ord instance?
mokus
+3  A: 

The short answer is: Haskell does this because that's how the language spec is written.

The long answer involves quoting from the GHC documentation language extensions section:

Any data type that can be declared in standard Haskell-98 syntax can also be declared using GADT-style syntax. The choice is largely stylistic, but GADT-style declarations differ in one important respect: they treat class constraints on the data constructors differently. Specifically, if the constructor is given a type-class context, that context is made available by pattern matching. For example:

data Set a where
    MkSet :: Eq a => [a] -> Set a

(...)

All this behaviour contrasts with Haskell 98's peculiar treatment of contexts on a data type declaration (Section 4.2.1 of the Haskell 98 Report). In Haskell 98 the definition

data Eq a => Set' a = MkSet' [a]

gives MkSet' the same type as MkSet above. But instead of making available an (Eq a) constraint, pattern-matching on MkSet' requires an (Eq a) constraint! GHC faithfully implements this behaviour, odd though it is. But for GADT-style declarations, GHC's behaviour is much more useful, as well as much more intuitive.

camccann
+1  A: 

I would like to point out that if you are worried that one could construct an object that requires constraints for its operations but doesn't for its creation, say mkFoo, you can always artifically put the constraint on the mkFoo function to enforce the use of the typeclass by people who use the code. The idea also extends to non mkFoo type functions that operate on Foo. Then when defining the module, don't export anything that doesn't enforce the constraints.

Though I have to admit, I don't see any use for this.

trinithis