Preamble
There are a number of meanings to the word polymorphism. I will give a couple of examples for each. Although my answer may somewhat overlap with other posts, I hope it may still be of some value.
Ad hoc polymorphism (function overloading)
What you asked for and for what you have received answers is an ad-hoc polymorphism, which basically is function overloading. As has been answered already, in Mathematica the idiomatic way to do this is by using patterns. Incidentally, some of the best examples of heavily overloaded functions in Mathematica are expression parsers of some kind. I will give here two examples.
Here is a function from this post - a parser to parse pattern arguments and extract pattern variables:
ClearAll[parse];
SetAttributes[parse, HoldAll];
parse[(Condition | PatternTest | Optional)[arg_, _]] := parse[arg];
parse[(HoldPattern | Optional)[arg_]] := parse[arg];
parse[Verbatim[Pattern][sym_, _]] := Hold[sym];
parse[Verbatim[Repeated][p_, ___]] := parse[p];
parse[(Blank | BlankSequence | BlankNullSequence)[___]] := Hold["NotAPatternVar"];
parse[(Longest | Shortest)[arg_, ___]] := parse[arg];
parse[Verbatim[PatternSequence][args___]] := parse[args];
parse[a_ /; AtomQ[Unevaluated[a]]] := Hold["NotAPatternVar"];
parse[args___] := Join @@ Map[parse, Unevaluated /@ Unevaluated[{args}]];
parse[f_[args___]] := {Hold[f], parse[args]};
And here is a function taken from the RLink implementation, which performs the conversion from RLink's internal form to the "user-friendly" form of an expression (typically received from R):
ClearAll[fromRDataType];
fromRDataType[atts_RAttributes]:=
Replace[atts,(aname_:>aval_):>aname:>Evaluate[fromRDataType[aval]],{1}];
fromRDataType[RVector[_,data_List,RAttributes[]]]:=
data;
fromRDataType[RVector[type_,data_List,a:RAttributes[atts__]]]:=
With[{dims="dim"/. {atts}},
fromRDataType[
RVector[
type,
(Transpose[unflatten[data,#1],Reverse[Range[Length[#1]]]]&)[
Reverse[fromRDataType[dims]]
],
DeleteCases[a,"dim":>_]
]
] /; dims=!="dim"
];
fromRDataType[RVector[type_,data_List,atts_RAttributes]]:=
RObject[data,fromRDataType[atts]];
fromRDataType[RNull[]]:= Null;
fromRDataType[RList[data_List,RAttributes[]]]:=
fromRDataType /@ data;
fromRDataType[RList[data_List,atts_RAttributes]]:=
RObject[fromRDataType[RList[data,RAttributes[]]],fromRDataType[atts]];
fromRDataType[r_RObject]:= r;
fromRDataType[RCode[code_,atts_RAttributes]]:=
RCode[code,fromRDataType[atts]];
fromRDataType[env_REnvironment]:= env;
fromRDataType[f_RFunction]:= f;
fromRDataType[_]:=
Throw[$Failed,error[fromRDataType]];
What is important for these constructs is to make sure that the definitions you create are mutually exclusive, if possible, and if they are not, then make sure that you give them in the right order, since automatic reordering can not always handle things automatically (in terms of relative generality of patterns).
It also often makes sense to have some catch-all (error-reporting or otherwise doing something) definition, to make sure that your pattern-match is exhaustive (in languages like OCaml the non-exhaustive pattern-match won't even compile. In Mathematica, you can effectively do the same by adding a catch-all case (although this will be postponed here until the run-time). Believe me, it is very important).
Parametric polymorphism
This is something Mathematica naturally has, because it is an untyped language, and a number of core functions work on any expressions (examples would include Map
,Apply
,Length
, etc). On the other hand, there is less to it in Mathematica than in some strongly-types languages, again because it is untyped and parametrization over types does not have here such a meaning.
In some sense, pattern-matching in Mathematica is general enough to provide a form of parametric polymorphism of the type similar to that of strongly-typed functional languages. For example, the following function to compute a quadratic norm of a tensor of arbitrary rank is made polymorphic over tensor "types":
norm[t_?ArrayQ]:= Sqrt[Total[Flatten[Abs[t]^2]]]
while you can restrict it to only numeric tensors by restricting the pattern as
norm[t_/;ArrayQ[t,_,NumericQ]]:= Sqrt[Total[Flatten[Abs[t]^2]]]
Note that even this restricted version is still parametrically polymorphic, over types Integer
, Real
, Rational
, Complex
, and also literal values such as Pi
, E
and other transcendental numbers. So, the notion of parametric polymorphism is naturally consistent in Mathematica with patterns serving as means to define types.
Note also that you can make functions parametrically-polymorphic in a more formal way, since you can define e.g.
norm[t_TensorType]:=...
where TensorType
may well be not a single type, but a class of types - which brings us also to the inclusion polymorphism (subtyping)
Inclusion polymorphism (subtyping)
This type of polymorphism is usually associated with object-oriented paradigm and languages which support it. Basically, it says that one shouldn't always need to know whether an object of some type or its subtype is being used, when some method is called. Arguably this is what gives the OO approach much of its power.
There are several way one can implement this type of polymorphism in Mathematica.
You can, for example, do this by wrapping your different strong types into an additional wrapper. Here is a simple example of this. Say you have two types circle
and disk
, both of which support the method getRadius
. The code may look like
ClearAll[circle, disk];
circle /: getRadius[circle[{_, _}, r_]] := r;
disk /: getRadius[disk[{_, _}, {rmin_, rmax_}]] := 1/2 (rmin + rmax);
Note that the getRadius
function has been overloaded on both types via UpValues
. It is more like a generic function than a method, to be precise, but we can as well treat it as a method here. Now, we can also add the catchall definition which raises and error:
ClearAll[getRadius];
getRadius[___] := Throw[$Failed];
Note that since it has been defined as a DownValue
for getRadius
, it will only fire after the UpValues
defined above for specific types are tried - which is what we need. So, we get an error only when we call getRadius
on some object which does not support this method.
This is not bad, but perhaps not quite good enough yet. You may now want to construct a super-type CircularFigure
, and augment the definitions for the types circle
and disk
, as follows:
ClearAll[circle, disk];
circle /: circularFigureQ[_circle] := True;
circle /: getRadius[circle[{_, _}, r_]] := r;
disk /: circularFigureQ[_disk] := True;
disk /: getRadius[disk[{_, _}, {rmin_, rmax_}]] := 1/2 (rmin + rmax);
and
ClearAll[GetRadius];
GetRadius[CircularFigure[f_?circularFigureQ]] := getRadius[f];
so that
GetRadius[CircularFigure[circle[{0, 0}, 5]]]
(* 5 *)
GetRadius[CircularFigure[disk[{0, 0}, {2, 4}]]]
(* 3 *)
This doesn't look like a big deal, but what it buys you is that the client of your code doesn't need to know anything about the actual representations of the types circle
and disk
(unlike for the getRadius
function), all they need to know is that it should be an object of type CircularFigure
. In fact, you can also make the predicate circularFigureQ
private, by defining a constructor like:
ClearAll[CircularFigure];
CircularFigure[Except[_?circularFigureQ]]:= Throw[$Failed, CircularFigure]
Then, all invalid objects will result in an exception thrown at the time of their construction, and you can simply define the client functions like
GetRadius[f_CircularFigure]:=...,
where all the implementation details have been completely hidden.
Summary
Mathematica pattern-matcher seems to be general enough to support all well-known forms of polymorphism. However, because even the standard notion of types does not exist in Mathematica in the same way as in most other languages, the distinction between these different forms here is perhaps more blurred than in the more traditional languages.