x = do
ints <- grabSomeIntegers
return (ints & map (*2) & filter (> 3) & map (-1))
x = fmap (map (- 1) . filter (> 3) . map (* 2)) grabSomeIntegers
... right? Nope, that doesn't have to be the case: Haskell supports more than just your $grandparent's parameterized types!
The vector package uses something called data families to have different in-memory representations for vectors of different types, without "letting go of the DRY" at all.
Here, Vector a is a data family: how a value of type Vector a "looks" in memory depends on a. So you'll notice a bunch of lines of the form
newtype instance Vector Int16 = MV_Int16 (P.Vector Int16)
Written using vectors, code like in your example is perfectly DRY; it would look like (this is untested code!) this:
combobulate :: Num a => IO (Vector a) -> IO (Vector a)
combobulate = do
as <- grabSomeIntegersFromNetwork
return (ints & V.map (*2) & V.filter (> 3) & V.map (-1))
The key is the data families: the types expose a simple, consistent API, hiding the actual guts of the memory representation from the user. The library author is free to perform whatever ugly/evil/unsafe manipulations she feels like behind the scenes on the internal representations, or not.
 Defining numeric operations on animals is left as an exercise to the careful reader.