What is the purpose of polymorphism?
Polymorphism makes a static type system more flexible without losing (significant) static type safety by loosening the conditions for type equivalence. The proof remains that a program will only run if it doesn't contain any type errors.
A polymorphic function or data type is more general than a monomorphic one, because it can be used in a wider range of scenarios. In this sense polymorphism represents the idea of generalization in strictly typed languages.
How does this apply to Javascript?
Javascript has a weak, dynamic type system. Such a type system is equivalent with a strict type system containing only one type. We can think of such a type as a huge union type (pseudo syntax):
type T =
| Undefined
| Null
| Number
| String
| Boolean
| Symbol
| Object
| Array
| Map
| ...
Every value will be associated to one of these type alternatives at run-time. And since Javascript is weakly typed, every value can change its type any number of times.
If we take a type theoretical perspective and consider that there is only one type, we can say with certainty that Javascript's type system doesn't have a notion of polymorphism. Instead we have duck typing and implicit type coercion.
But this shouldn't keep us from thinking about types in our programs. Due to the lack of types in Javascript we need to infer them during the coding process. Our mind have to stand in for the missing compiler, i.e. as soon as we look at a program we must recognize not only the algorithms, but also the underlying (maybe polymorphic) types. These types will help us to build more reliable and more robust programs.
In order to do this properly I am going to give you an overview of the most common manifestations of polymorphism.
Parametric polymorphism (aka generics)
Parametric polymorphism says that different types are interchangeable because types doesn't matter at all. A function that defines one or more parameters of parametric polymorphic type must not know anything about the corresponding arguments but treat them all the same, because they can adopt to any type. This is quite restricting, because such a function can only work with those properties of its arguments that are not part of their data:
// parametric polymorphic functions
const id = x => x;
id(1); // 1
id("foo"); // "foo"
const k = x => y => x;
const k_ = x => y => y;
k(1) ("foo"); // 1
k_(1) ("foo"); // "foo"
const append = x => xs => xs.concat([x]);
append(3) ([1, 2]); // [1, 2, 3]
append("c") (["a", "b"]); // ["a", "b", "c"]
Ad-hoc polymorphism (aka overloading)
Ad-hoc polymorphism says that different types are equivalent for a specific purpose only. To be equivalent in this sense a type must implement a set of functions specific to that purpose. A function that defines one or more parameters of ad-hoc polymorphic type then needs to know which sets of functions are associated to each of its arguments.
Ad-hoc polymorphism makes a function compatible to a larger domain of types. The following example illustrates the "map-over" purpose and how types can implement this constraint. Instead of a set of function the "mappable" constraint only includes a single map
function:
// Option type
class Option {
cata(pattern, option) {
return pattern[option.constructor.name](option.x);
}
map(f, opt) {
return this.cata({Some: x => new Some(f(x)), None: () => this}, opt);
}
};
class Some extends Option {
constructor(x) {
super(x);
this.x = x;
}
};
class None extends Option {
constructor() {
super();
}
};
// ad-hoc polymorphic function
const map = f => t => t.map(f, t);
// helper/data
const sqr = x => x * x;
const xs = [1, 2, 3];
const x = new Some(5);
const y = new None();
// application
console.log(
map(sqr) (xs) // [1, 4, 9]
);
console.log(
map(sqr) (x) // Some {x: 25}
);
console.log(
map(sqr) (y) // None {}
);
Subtype polymorphism
Since other answers already cover subtype polymorphism I skip it.
Structural polymorphism (aka strutrual subtyping)
Structural polymorphism says that different types are equivalent, if they contain the same structure in such a way, that one type has all the properties of the other one but may include additional properties. That being said, structural polymorphism is duck typing at compile time and certainly offers some additional type safety. But by claiming that two values are of the same type just because they share some properties, it completely ignores the semantic level of values:
const weight = {value: 90, foo: true};
const speed = {value: 90, foo: false, bar: [1, 2, 3]};
Unfortunately, speed
is considered a subtype of weight
and as soon as we compare the value
properties we are virtually comparing apples with oranges.