7 ms·
I think the biggest advantage of parametric polymorphism comes from program analysis, not just increased capability. The notion of parametricity depends crucial
by tel 13y ago
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.
- andolanra 13y agoI'm going to note that even if Go added some kind of parametric polymorphism at this point, it still wouldn't have the same advantages as parametric polymorphism in Haskell and related languages. In Go, you can branch on types at runtime, so in a hypothetical generic Go, you could write something which appears to only ever return its argument, but does something special if the argument is of a particular type: func<T> id(t T) T { switch t := T.(type) { case int: return t + 1; default: return t; } } The ability to examine on types at runtime can enable some interesting programs, but it also means that you don't get guarantees like you do in languages like Haskell. An example of this biting someone in practice is this blog post[1], where the author reasoned that because a library function expected an io.Reader, all it would do is use the Read method as exposed by Reader. In reality, though, the function was able to branch on the type and determine whether it also exposed a Close method, and called that if it could. That kind of feature destroys the useful static guarantees provided by parametric polymorphism in other languages like ML or Haskell. [1]: http://how-bazaar.blogspot.co.nz/2013/07/stunned-by-go.html http://how-bazaar.blogspot.co.nz/2013/07/stunned-by-go.html
- tel 13y agoAs a second addendum, while andolanra's comment is completely true, you can actually do type dispatch in Haskell using the Typeable machinery; however, the type system requires that you mark your types when this occurs. what :: Typeable a => a -> a Here `what` is no longer guaranteed to be `id`. Instead, we can translate it like this what :: (a -> TypeRep) -> a -> a where `TypeRep` is, as it sounds, a representation of the type of a returned dynamically. This means that we can do careful coercions to, say, be `id` unless `a` is `Int` in which case we add `1`. Suffice to say that the degree of guarantee breaking that this kind of code invites makes Typeable a very specialized library that's almost never recommended without a "unless you really know what you're doing" kind of warning.
- chimeracoder 13y ago> In Go, you can branch on types at runtime, so in a hypothetical generic Go, you could write something which appears to only ever return its argument, but does something special if the argument is of a particular type I'm not sure why you bring this up - nearly every language that enables reflection lets you do this, including Haskell[0]. No decidable static type system can ensure correct semantics for all use cases, including even badly written code[1]. [0] And you don't even need reflection to do this, per se; it just makes it more convenient. [1] One could design a static type system that did, but it would be undecidable.