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.
What's a typeclass in Haskell? A typeclass defines a set of methods that is shared across multiple types. For a type to belong to a typeclass, it needs to implement the methods of that typeclass. These implementations are ad-hoc: methods can have different implementations for different types.
Overloaded Functions. A polymorphic function is called overloaded if its type contains one or more class constraints.
Haskell classes are roughly similar to a Java interface. Like an interface declaration, a Haskell class declaration defines a protocol for using an object rather than defining an object itself. Haskell does not support the C++ overloading style in which functions with different types share a common name.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With