struct Person<T> {
age: T,
}
Is parametric polymorphism and valid Rust. struct Person<T> {
age: T,
}
Is parametric polymorphism and valid Rust.There's also more minor things like https://github.com/rust-lang/rfcs/pull/1210#issuecomment-179...
If you ask all the developers out there to describe parametric and ad-hoc polymorphism I think a vast majority would give the example of a type parameter (e.g., Java generics or C++ templates) for parametric polymorphism and Java interfaces or Haskell's classes for ad-hoc polymorphism. I can even quote directly from Pierce (Types and Programming Languages):
> Parametric polymorphism, the topic of this chapter, allows a single piece of code to be typed "generically," using variables in place of actual types, and then instantiated with particular types as needed. Parametric definitions are uniform: all of their instances behave the same.
I think Rust and the aforementioned languages fit this definition. Outside of a specific compiler issue, claiming otherwise seems to only confuse the issue, especially for those just casually reading and not familiar with programming language theory.
In practice it is often useful to drop our demands for rigour by a bit. But any reasonable definition of parametric polymorphism _has_ to exclude C++ templates.
C++ templates are much closer to the definition of ad-hoc polymorphism.
My practical less-than-rigorous rule-of-thumb for parametric polymorphism amounts to something like: whenever Wadler's Theorems for Free paper applies. https://ttic.uchicago.edu/~dreyer/course/papers/wadler.pdf
Theorems for Free means that eg the type of the identity function (forall a . a -> a) guarantees that even if you supply it a bool, it can't magically turn into an implementation for `not`, or multiply all integers by 2.
Which doesn't mean it's terrible to get it wrong, just want to be clear about what Rust does and does not have. It is important because these kinds of definitions either hold or they don't; "sorta kinda mostly parametric" isn't the way folks tend to think about this. Which makes sense, because they're interested in proofs and formal definitions.
Yes, Pierce is great! But the issue is:
> all of their instances behave the same.
This is not true in Rust, as shown in my comment and the other replies. We have accepted APIs that break this property, and we have other language features on the way that break this property.
fn foo<T>() -> usize {
std::mem::size_of::<T>()
}
foo::<u8>(); // 1
foo::<u16>(); // 2The _behavior_ of this function is the same for all types, the _output_ is different. That is, for all types, the function body is the same. Maybe there is a more abstract definition of parametric polymorphism you are using, but as I said above, this seems pedantic.
fn foo<T>() -> usize {
let x = std::mem::size_of::<T>();
if x % 2 == 0 {
panic!();
}
}
foo::<u8>(); // 1
foo::<u16>(); // panic
That the body is the same isn't necessarily the issue at hand (though of course it's still a useful property in its own right), what matters is that reasoning about what this function will do requires knowing which specific types it is used with.> this seems pedantic
The first code example is merely the simplest demonstration, in the wild I would expect lots of `size_of` in generic contexts to result in type-dependent behavior somehow.
I'm not saying this is necessarily a very bad thing, nor do I have strong opinions on the usefulness of strict parametricity (which AFAIK Haskell doesn't have either). But in discussions relevant to parametricity, it's useful to know the ways a given language can subvert it (and Rust will further encourage it to be subverted, once the specialization feature is developed).
In practice Haskell seems to have pretty strong views on enforcing parametric polymorphism, doesn't it?
Haskell gives you ad-hoc polymorphism via typeclasses and there are also existential types and GADTs etc, if you need those. But once you declare something to abide by parametric polymorphism, you are expected to keep your end of the bargain.
(Yes, you could violate the pact via some unsafePerformIO trickery, but that's considered bad form.)
If you functions routinely violate those limits as a matter of course, those guarantees are useless.
I'm all for abusing notation and terminology a bit when it makes sense in practice, but loosening our definitions too much risks making them useless, too.
In practice in Haskell, I often only need a helper function for eg integers, but when the implementation allows, I will give the function the most parametric-polymorphic type that fits, because that makes the readers job easier:
Just like an invocation of `filter` is easier to read than a for-loop, because `filter` is strictly less powerful than the loop.
(In addition, the more general type serves to give the compiler a hint, so it can yell at me in case I accidentally do an operation on the integer that I didn't mean to.)