Learn You a Haskell for Great Good! - Miran Lipovaca [53]
Another example of a parameterized type that you’ve already met is Map k v from Data.Map. The k is the type of the keys in a map, and v is the type of the values. This is a good example of where type parameters are very useful. Having maps parameterized enables us to have mappings from any type to any other type, as long as the type of the key is part of the Ord type class. If we were defining a mapping type, we could add a type class constraint in the data declaration:
data (Ord k) => Map k v = ...
However, it’s a very strong convention in Haskell to never add type class constraints in data declarations. Why? Well, because it doesn’t provide much benefit, and we end up writing more class constraints, even when we don’t need them. If we put the Ord k constraint in the data declaration for Map k v, we still need to put the constraint into functions that assume the keys in a map can be ordered. If we don’t put the constraint in the data declaration, then we don’t need to put (Ord k) => in the type declarations of functions that don’t care whether the keys can be ordered. An example of such a function is toList, which 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 need to be toList :: (Ord k) => Map k a -> [(k, a)], even though the function doesn’t compare keys by order.
So don’t put type constraints into data declarations, even if it seems to make sense. You’ll need to put them into the function type declarations either way.
Vector von Doom
Let’s implement a 3D vector type and add some operations for it. We’ll make it a parameterized type, because even though it will usually contain numeric types, it will still support several of them, like Int, Integer, and Double, to name a few.
data Vector a = Vector a a a deriving (Show)
vplus :: (Num a) => Vector a -> Vector a -> Vector a
(Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n)
dotProd :: (Num a) => Vector a -> Vector a -> a
(Vector i j k) `dotProd` (Vector l m n) = i*l + j*m + k*n
vmult :: (Num a) => Vector a -> a -> Vector a
(Vector i j k) `vmult` m = Vector (i*m) (j*m) (k*m)
Imagine a vector as an arrow in space—a line that points somewhere. The vector Vector 3 4 5 would be a line that starts at the coordinates (0,0,0) in 3D space and ends at (and points to) the coordinates (3,4,5).
The vector functions work as follows:
The vplus function adds two vectors together. This is done just by adding their corresponding components. When you add two vectors, you get a vector that’s the same as putting the second vector at the end of the first one and then drawing a vector from the beginning of the first one to the end of the second one. So adding two vectors together results in a third vector.
The dotProd function gets the dot product of two vectors. The result of a dot product is a number, and we get it by multiplying the components of a vector pairwise and then adding all that together. The dot product of two vectors is useful when we want to figure out the angle between two vectors.
The vmult function multiplies a vector with a number. If we multiply a vector with a number, we multiply every component of the vector with that number, effectively elongating (or shortening it), but it keeps on pointing in the same general direction.
These functions can operate on any type in the form of Vector a, as long as the a is an instance of the Num type class. For instance, they can operate on values of type Vector Int, Vector Integer, Vector Float, and so on, because Int, Integer, and Float are all instances of the