Parametric polymorphism
| Type variables | |
| forall and free theorems | |
| Write it once | |
| When to specialise |
Type variables
A lower case letter in a signature stands for any type. The function must then work for every choice, because it has no way to inspect the value:
identity :: a -> a
identity x = x
pairUp :: a -> b -> (a, b)
pairUp x y = (x, y)
identity cannot add one, print the value, or compare it with anything, because none of those operations exist for an unknown type. That restriction is the feature.
forall and free theorems
The signature means for all a, and you can write the quantifier explicitly. It changes nothing here but makes the promise visible:
identity :: forall a. a -> a
identity x = x
A famous consequence is that a function [a] -> [a] can only reorder, drop, or duplicate elements. It cannot invent an a, because it has none. Reading a signature therefore tells you a lot about the implementation before you open it.
Write it once
Generic helpers replace a family of near-identical functions. These three work for strings, lists of numbers, lists of records, and anything else:
firstOr :: a -> [a] -> a
firstOr d [] = d
firstOr _ (x:_) = x
lastOf :: [a] -> Maybe a
lastOf [] = Nothing
lastOf xs = Just (foldl1 (\_ y -> y) xs)
The default value in firstOr is the price of staying total. Returning Maybe instead, as lastOf does, moves that decision to the caller.
When to specialise
Generality is not free. A function that needs Ord should say so in the constraint rather than pretend to be fully polymorphic, and a hot loop may need a concrete type so GHC can unbox the numbers:
-- too general to sort
sortAny :: [a] -> [a]
-- honest about what it needs
sortAny :: Ord a => [a] -> [a]
sortAny = sort
Start as general as the body allows, then add the narrowest
constraint that compiles. The signature is documentation the compiler
checks.
Next:
Maybe and null safety
Article author: Arthur Isaev