I think the biggest advantage of parametric polymorphism comes from program analysis, not just increased capability. The notion of parametricity depends crucially on parametric polymorphism and is super useful. Parametricity is the property that says greater polymorphism means lesser variation in implementation. Highly parametric functions can be almost completely described by their types along meaning it provides incredibly high value as documentation.
The most common simple example is reasoning about the following pair of functions
f :: a -> a g :: Int -> Int
While (f > g) obviously, we can completely characterize the behavior of `f` while we know almost nothing at all about `g`. Up to non-termination, `f`
must be `id` while `g` can be any recursive function on integers.
g a = 3
f a = a g a = a * 2
g a = if (a == 0) then 1 else a * g (a - 1)
If you hold in advance the triviality of this example, then we can already notice how much better documentation `f`'s type provides than `g`'s. We also can produce a list of properties and laws about `f`'s behavior that we cannot easily do with `g`. This means that I'm able to reason about `f` better and more confidently.
---
So we can take it up a notch. Consider Functors in Haskell. In order to avoid the full generality, let's just say Functor is the container interface and types like [], Set, and Map instantiate it. This gives them the function
fmap :: (a -> b) -> f a -> f b
where `f` is specialized to the particular container of interest
fmap :: (a -> b) -> [a] -> [b]
fmap :: (a -> b) -> Set a -> Set b
fmap :: (a -> b) -> Map k a -> Map k b -- for some key type k
Now what can we reason about `fmap`s knowing only their type? Well, since the type `a` contained inside each container is unknown (or, to be more clear, any implementation of fmap must work for any type `a` and thus it's "unknown" in that we can't take advantage of information about it) then the
only thing `fmap` is capable of doing is applying the first function to those types.
Furthermore, since every mention of `a` vanishes in the result of the function we know that each value in the input container must either be transformed by the input function or dropped.
-- a specific example, not a general function
fmap f [a, b, c] = [f a, f b, f b]
In this way, parametricity has pruned down the possibilities of functions that can implement `fmap` a great deal. If `a` or `b` were specialized to specific types then this would not be the case as we'd have a large variety of specific functions that could apply to each one of them.
notFmap :: (Int -> Char) -> [Int] -> [Char]
notFmap f [n1, n2, n3] = [f (n1 + 2), head (show n2), chr n3] ++ "hello world!"
Indeed, in order to ensure that type-checking `fmap`s behave correctly we say they must follow a few laws. First, they respect function composition and second they respect identity
fmap g (fmap f x) == fmap (g . f) x
fmap id x == id x
And we only ever need to check the first one because the second one is already guaranteed by a more sophisticated parametricity argument.
fmap f [] = []
fmap f (x:xs) = f x : fmap f xs
---
The take away is that while you can definitely write something that "maps over containers" without using parametric polymorphism, you have a harder time taking advantage of parametricity to have the types provide guarantees about the safe behavior of functions. Types (and in particular type inference) can sometimes make things more expressive, but their primary power lies in making things easier to understand and reason about.