What is polymorphism in Javascript?
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.
Polymorphism is one of the tenets of Object Oriented Programming (OOP). It is the practice of designing objects to share behaviors and to be able to override shared behaviors with specific ones. Polymorphism takes advantage of inheritance in order to make this happen.
In OOP everything is considered to be modeled as an object. This abstraction can be taken all the way down to nuts and bolts for a car, or as broad as simply a car type with a year, make, and model.
To have a polymorphic car scenario there would be the base car type, and then there would subclasses which would inherit from car and provide their own behaviors on top of the basic behaviors a car would have. For example, a subclass could be TowTruck which would still have a year make and model, but might also have some extra behaviors and properties which could be as basic as a flag for IsTowing to as complicated as the specifics of the lift.
Getting back to the example of people and employees, all employees are people, but all people are not employees. Which is to say that people will be the super class, and employee the sub class. People may have ages and weights, but they do not have salaries. Employees are people so they will inherently have an age and weight, but also because they are employees they will have a salary.
So in order to facilitate this, we will first write out the super class (Person)
function Person(age,weight){
this.age = age;
this.weight = weight;
}
And we will give Person the ability to share their information
Person.prototype.getInfo = function(){
return "I am " + this.age + " years old " +
"and weighs " + this.weight +" kilo.";
};
Next we wish to have a subclass of Person, Employee
function Employee(age,weight,salary){
this.age = age;
this.weight = weight;
this.salary = salary;
}
Employee.prototype = new Person();
And we will override the behavior of getInfo by defining one which is more fitting to an Employee
Employee.prototype.getInfo = function(){
return "I am " + this.age + " years old " +
"and weighs " + this.weight +" kilo " +
"and earns " + this.salary + " dollar.";
};
These can be used similar to your original code use
var person = new Person(50,90);
var employee = new Employee(43,80,50000);
console.log(person.getInfo());
console.log(employee.getInfo());
However, there isn't much gained using inheritance here as Employee's constructor is so similar to person's, and the only function in the prototype is being overridden. The power in polymorphic design is to share behaviors.
As explained in this other answer, polymorphism has different interpretations.
The best explanation on the subject that I've ever read is an article by Luca Cardelli, a renowned type theorist. The article is named On Understanding Types, Data Abstraction, and Polymorphism.
What Is it?
Cardelli defines several types of polymorphism in this article:
- Universal
- parametric
- inclusion
- Ad-hoc
- oveloading
- coercion
Perhaps in JavaScript, it is a bit more difficult to see the effects of polymorphism because the more classical types of polymorphism are more evident in static type systems, whereas JavaScript has a dynamic type system.
So, for instance, there is no method or function overloading or automatic type coercions at compile time in JavaScript. In a dynamic language, we take most of these things for granted. Neither we need something like parametric polymorphism in JavaScript due to the dynamic nature of the language.
Still, JavaScript has a form of type inheritance that emulates the same ideas of subtype polymorphism (classified as inclusion polymorphism by Cardelli above) in a similar way to what we typically do in other object-oriented programing languages like Java or C# (as explained in another answer I shared above).
Another form of polymorphism very typical in dynamic languages is called duck typing.
It is a mistake to believe that polymorphism is only related to object-oriented programming. Other programming models (functional, procedural, logic, etc.) offer different forms of polymorphism in their type systems, probably in a way a bit unfamiliar to those only used to OOP.
Why We Need It?
Polymorphism foster many good attributes in software, among other things it fosters modularity and reusability and makes the type system more flexible and malleable. Without it, it would be really difficult to reason about types. Polymorphism makes sure that one type can be substituted by other compatible ones provided that they satisfy a public interface, so this also fosters information hiding and modularity.
How Does it Work?
This is not simple to answer, different languages have different ways to implement it. In the case of JavaScript, as mentioned above, you will see it materialize in the form of type hierarchies using prototypal inheritance and you can also exploit it using duck typing.
The subject is a bit broad and you opened too many questions in a single post. Perhaps it is best that you start by reading Cardelli's paper and then try to understand polymorphism irrespective of any language or programming paradigm, then you will start making associations between the theoretical concepts and what any particular language like JavaScript has to offer to implement those ideas.