"Polymorphism is the quality of that which has or can have different forms" (R.A.E. Dictionary).
"Polymorphism is the quality or state of existing in or assuming different forms" (Merriam-Webster Dictionary).
Concept
Polymorphism means "many forms". It is the opposite of monomorphism and refers, in general, to the occurrence of elements of different types in some language expression.
If an expression has different meanings depending on the types of its components, the polymorphic expression is said to be "overloaded" (of semantics, it goes without saying). If the semantics are maintained throughout, the polymorphism is without overloading.
Types of polymorphic expressions
Polymorphic functions and polymorphic procedures are those that can be evaluated with arguments of different types, as opposed to monomorphic procedures and functions, where the arguments are of a fixed type. An overloaded function or procedure is one that performs different actions depending on the types of the arguments.
Examples:
In PostScript −a language oriented to describe print pages in a device-independent manner)− polymorphic (non-overloaded) operators are used:
length (length of an object).
forall (traverse all elements of an object).
get (read object from the stack).
put (place object on the stack).
These operations are generic and act on any type of object (arrays, strings, numbers, dictionaries, etc.).
In the Ada language [Barnes, 2013], the so-called "generic" functions are polymorphic, although polymorphism is limited.
ML [Milner, 1984] is a language with polymorphic functions and allows other polymorphic functions to be easily developed.
In the lambda calculus [Church, 1941], which is the foundation of functional languages, functions can be defined and applied independently of the types of the arguments.
A polymorphic operator is one that admits different types of arguments.
In an overloaded operator the semantics differ depending on the types of the arguments. What is intended with overloaded operators is twofold:
To avoid having to define new operators.
To facilitate the conceptual connection with another similar but different operation.
Examples:
The arithmetic operators of most programming languages.
These operators are overloaded, because for example the "+" operator in A+B has different meanings, depending on whether A and B are integers or real numbers, complex numbers, vectors, sequences or matrices. In vectors and matrices it means adding each of the components. In sequences it can mean to concatenate.
In the C language, the "&" (pointer) operator is polymorphic not overloaded, since it can be applied to any type of operand.
A polymorphic identifier is one that can have a set of possible types. As a consequence, an expression with polymorphic identifiers can also have a resulting set of types. For example, in PostScript you can assign any type of object to a variable.
A polymorphic structure is one that is composed of different types of elements. For example, lists in Lisp can contain different types of elements (numbers, text, etc.).
A polymorphic symbol is one that can appear in different contexts. An overloaded symbol is one that has different meanings depending on the context. The use of the same symbol to express different semantics constitutes a mere inconsistency.
Examples:
Parentheses are often used with several semantics: to clear ambiguities in expressions, to specify the arguments of a function or procedure, and to define sequences or lists.
In the Ada language, parentheses are overloaded, since the expression A(I) can have 3 interpretations:
The I-th element of the array A.
A call to function A with argument I.
An explicit conversion of the expression I to type A.
In object-oriented programming, polymorphism is a fundamental concept, which has two readings:
The different sensitivities of an object to receive different types of messages. We speak of polymorphic objects.
The possibility that the same message sent to different objects produces a different action in these objects. For example, the message "+" sent to an object that is an integer means addition, whereas if the object is a sequence it could mean concatenation.
Implicit and explicit polymorphism
They apply to procedure headers. When type parameters are defined (and therefore in the call arguments), we speak of explicit polymorphism.
In implicit polymorphism there is no type parameter, being the language processor the one in charge of "discovering" the types of the arguments.
Resolution
Polymorphism with overloading requires an additional process called "resolution", which consists in determining the semantics to apply among the different possible ones. This resolution can take place at compile time (static polymorphism) or at run time (dynamic polymorphism).
Resolution can be a relatively simple process (for example in arithmetic operators by looking at arguments) or more complex. Resolution is more difficult if identifier declarations do not exist or are optional.
In overload resolution, Tennenbaum [1974] distinguishes between:
Forward resolution. It determines the set of possible types of an operator from its operands.
Backward resolution. Based on the expected type according to the context.
For example, in Ada, overload resolution is performed by one forward and one backward pass.
Which type of polymorphism is of more interest?
Polymorphism with overloading has two drawbacks:
It creates confusion, as there are different semantics at play.
A resolution process has to be performed, which consumes resources.
In contrast, polymorphism without overloading maintains semantics at all times and also consistency. Therefore, we are only interested in polymorphism without overloading.
Advantages of polymorphism without overloading
It facilitates the specification of algorithms, orienting them to their conceptual or semantic aspect, freeing the coding of low-level aspects.
Generalizes coding. The same algorithm can be used to access and manipulate different data structures, regardless of the type of the constituent elements. For example, a function that returns the length of a list, that accesses an element of a list, etc. regardless of the type of its elements.
Simplifies programming. No need to code different procedures for each type of element.
It implies a relaxation of strict operational standards or rules and thus provides a greater degree of freedom and flexibility.
Polymorphic operations bring out a common essence, an abstraction ultimately that is independent of the elements on which it operates.
It provides an indispensable combinatorial system to relate elements of different types, favoring creativity.
MENTAL and Polymorphism
Semantic Primitives without Overloading
MENTAL is a polymorphic language without overloading. For example, in the expression x+y, x and y can be any type of expression, but the semantics are preserved. That is, if x and y are two matrices and are equal, the result is 2*x. And if we add two expressions equal to 〈x+y〉, the result is 2*〈x+y〉.
All semantic primitives are polymorphic without overloading, i.e., the semantics associated with each primitive is always the same. That is, addition is always addition, substitution is always substitution, and so on. It must necessarily be so, since the universality of primitives demands it. And furthermore, polymorphism is intimately related to freedom, to the possibility of forming expressions without restrictions. When a polymorphic expression has no possibility to evaluate itself, it evaluates itself.
Examples of polymorphic expressions with semantic primitives
( a/b 17.5 〈( f(xy) = x+y+7 )〉 ) // sequence
{ a/b f(3 4) 〈( f(xy) = x+y+7 )〉 } // set
( 〈( f(xy) = x+y+7 )〉 + 12 ) // sum
(x+y+z = {a b c}) // substitution
( 〈( f(xy) = x+y+7 )& 〉 = 33 ) // substitution
color/〈( f(xy) = x+y+7 )〉 // particularization
(a/b ≡ 33) // equivalence
(33 ← a/b) // condition
(a/b 17.5 "text")↓ // access to the contents of a stream
Derived operations and polymorphism
Derived operations can be fully polymorphic or partially polymorphic, depending on their definitions. When variables are of a certain type, that indicates that type is necessarily required to make sense. If the type does not conform to the intended type, the expression is self-evaluating. Examples:
The derived operation x∪y indicates union of the expressions x and y, which must be of the same type (sequences or sets).
abc∪175 // ev. abc175
{a b c}∪{1 7 5} // ev. {a b c 1 7 5}
The derivative x★n indicates repeating x n times. The expression x can be any, but n indicates that it has to be a natural number. If n is not a natural number, the expression evaluates itself.
The derivative n1...n2 indicates the open sequence of numbers between n1 and n2. In this case the initial and final values have to be integers or summable expressions.
An integer, for example 1234, is a sequence of digits, because it represents, in a compact form, the sequence (1 2 3 4). Depending on the operation, it is treated as a number or as a sequence. Examples:
1234*5 // ev. 6170 (1234 is treated as an integer)
(1234 ∪ ab) // ev. 1234ab (1234 is treated as sequence)
Condition as existence
An expression that acts as a condition is interpreted as existence. Example:
The expression (x ← a=b) is interpreted as x if the equality a=b exists (is satisfied).
Operators with attributes
In order to simplify the number of operators, the same operator can be used with different attributes to facilitate the conceptual connection to the operator. For example,
+/v as "vector sum".
+/m as "matrix sum".
*/e as "scalar product
Examples:
Vector sum:
〈( x +/v y) =: ( [x\⌊1…x#⌋ + y\⌊1…y#⌋] ) )〉
(a = (11 22 33))
(b = (1 2 3))
(a +/v b) // ev. (11+1 + 22+2 + 33+3) ev. (12 24 36)