17
$\begingroup$

(For educational purposes) I defined the following functions:

Translation (in $\mathbb{R}^2$):

trM[vec_] := {{1, 0, vec[[1]]}, {0, 1, vec[[2]]}, {0, 0, 1}}

Rotation (in $\mathbb{R}^2$):

rotM[angle_] := {{Cos[angle], -Sin[angle], 0}, {Sin[angle], Cos[angle], 0}, {0, 0, 1}}

Rotation around a point (in $\mathbb{R}^2$):

rotM[pt_, angle_] := trM[pt].rotM[angle].trM[-pt]

So far so good, now I want a function:

rotM[angle_, n_] := 
  {{Cos[n angle], -Sin[n angle], 0}, {Sin[n angle], Cos[n angle], 0}, {0, 0, 1}}

Now

 rotM[angle_, n_] 

and

rotM[pt_, angle_] 

share — in Mathematica — the same signature, which breaks the polymorphism I intended to use, which then leads to the following question.

How can I type-check the arguments of a Mathematica function to enforce polymorphism?

$\endgroup$
1

3 Answers 3

10
$\begingroup$

At a minimal level you could discriminate like this

f[angle_ n_] := ...
f[pt : {_, _}, angle_] := ...

But if you want to be really picky, you could limit your pt argument to only except a list of two elements, both of which are numeric objects, but neither of which is a complex number. This can be done by defining a new argument pattern

pt2D = {Repeated[Except[_Complex, _?NumericQ], {2}]};

and using the pattern in a function definition such as

f[v : pt2D] := v

then

pts = {{1, 2}, {1., 2.}, {1., 2}, {1, π}, 1, {1}, {1, I}, {1, 2, 3}};
f /@ pts

gives

{{1, 2}, {1., 2.}, {1., 2}, {1, π}, f[1], f[{1}], f[{1, I}], f[{1, 2, 3}]}

Note both the wide acceptance of forms that have the structure of 2D points and the rejection of forms that don't.

$\endgroup$
1
  • $\begingroup$ Beautiful solution. $\endgroup$ Oct 13, 2013 at 6:51
13
$\begingroup$

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.

$\endgroup$
9
$\begingroup$

You can always use the Head of the argument as a type checker. For example if n is an Integer and angle is real, then:

rotM[angle_Real, n_Integer] := ...

rotM[pt_Real, angle_Real] := ...

Will ensure that the two are not identical. Now, if they are all Real, you can always define one to take a spurious third argument that's always the same e.g. for 1D points

rotM[pt_Real, angle_Real, 0] := ... 
vs
rotM[pt_Real, angle_Real, 1] := ... 

For 2D points in Real space one can define:

rotM[pt : {_Real, _Real}, angle_] :=

You can have a variation of the above with Complex, a mixture of Real and Complex or even Integers

rotM[pt : {_Real | _Integer, _Real | _Integer}, angle_] :=

You can also use _?NumericQ for approximate or exact numerical expressions or built in mathematical numerical constants such as Pi, GoldenRatio etc.

rotM[pt : {_?NumericQ, _?NumericQ}, angle_] :=

You can mix and match:

rotM[pt : {_Real, _Integer}, angle_] :=

You can also put conditions on the arguments:

rotM[pt_, angle_] /; Element[pt, Real] && angle >= 0 := ...

Of course you can use Options if they become more complex.

$\endgroup$
5
  • $\begingroup$ What do you mean by the use of options? ( The defs I actually use are in fact more complex ) $\endgroup$ Oct 12, 2013 at 10:31
  • $\begingroup$ @ndroock1 this is a good post about options. $\endgroup$
    – Murta
    Oct 12, 2013 at 11:13
  • $\begingroup$ This doesn't quite work. Consider, g[v_Real] := v; pt = {1., 2.}; g @ pt which gives g[{1., 2.}] $\endgroup$
    – m_goldberg
    Oct 12, 2013 at 11:57
  • $\begingroup$ @Murta. Thanks for the link, I was ready to sleep when I posted this answer. $\endgroup$
    – RunnyKine
    Oct 12, 2013 at 15:57
  • $\begingroup$ @MichaelE2. Thanks, I forgot about that. $\endgroup$
    – RunnyKine
    Oct 12, 2013 at 17:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.