13
$\begingroup$

I'm wondering how to properly assess the type of arguments passed to functions defined with :=. I want my functions to be the most efficient, unambiguous and clear as possible.

When hacking together a function, I don't really care about the type of each argument passed to that function. I'll use an example taken from the documentation.

A first function draft would look like this:

>> g[x_] := Prime[x] - x

Which works fine as long as the type of the argument passed to it is compatible with Prime. The following is inappropriate behaviour, and should be fixed by changing the function definition:

>> {g[10], g["z"]}
<< {19, -"z" + Prime["z"]}

Here's where I don't know how to correctly proceed. Once I make sure my function works, by providing it arguments of the correct type, I can change its definition to

>> g[x_Integer] := Prime[x] - x

or

>> g[x_?IntegerQ] := Prime[x] - x

which both work as expected.

What's the difference between the two definitions, and which one should be used for maximum efficiency, robustness and cleanliness?

Example with one of my functions

This functions generates a set of points that, when passed to Line, draw a zig-zag. This case is more ambiguous than the previous.

I can define it this way, which works:

>> contactCoordinates[nCtc_Integer, x0_Integer, y0_Integer, 
   offset_?NumberQ] := 
    Table[{x, y0}, {x, x0, offset*nCtc, offset}]~Riffle~
     Table[{x, y0 + offset/2}, {x, x0 + offset/2, offset*nCtc, offset}]

I could also replace _Integer by _?IntegerQ, with no noticeable difference. The problem is with _?NumberQ. I can't define the function using offset_Number - I have either to use offset_?NumberQ or offset_Real. Is one better than the other?


  1. http://reference.wolfram.com/mathematica/tutorial/PatternsForSomeCommonTypesOfExpression.html
  2. Functions vs. patterns
  3. https://stackoverflow.com/q/8922966/1142160
$\endgroup$
1

4 Answers 4

18
$\begingroup$

The differences between using _Integer and _?IntegerQ as to efficiency, robustness and cleanliness in my view are:

Efficiency

_Integer is more efficient. It belongs to what @LeonidShiffrin calls syntactic patterns and the evaluator doesn't need to be called to check if the pattern fits. On the other hand, ? and /; require a call to the evaluator

Robustness

Both have their issues. _?IntegerQ will leak evaluation in cases where you might not want to. For example

SetAttributes[f, HoldAll];
f[_?IntegerQ]:=8;

Now, evaluating f[Print@9] will print the number 9, and that wouldn't happen with _Integer. This isn't only an issue if your symbol has a holding attribute, because the user might always call the function using Unevaluated and expect it not to leak

_Integer, however, has the disadvantage of matching something like f[Integer[Plot[Sin[x], {x, 0, 10}]]] which is clearly not a good thing

The most robust solution I see is to fix IntegerQ using

f[_?(Function[i, IntegerQ@Unevaluated@i, HoldFirst])]:=8;

or to fix _Integer using

f[_Integer?(Function[i, AtomQ@Unevaluated@i, HoldFirst])]:=8;

If robustness is the priority, you probably can't avoid a call to the evaluator. It would be practical and neat to name a function

makeHolding[f_] := Function[Null, f@Unevaluated@##, HoldAll]
holdAtomQ= makeHolding[AtomQ]

and then just use f[_Integer?holdAtomQ], f[_Real?holdAtomQ], etc, or f[_?(makeHolding[IntegerQ])]

Cleanliness

Personally I like _Integer more in this regard but I think this is totally up to you


As to your example, all that comes to mind is the reminder that NumberQ won't match numbers like Pi while NumericQ will.

Also, as a sidenote, graphics primitives use real coordinates, and if you give them exact coordinates they have to save both versions of the numbers, in case you want to recover them. If you are not interested in keeping the original exact values in the front end, it may make sense to convert the parameters to approximate reals whatever form they came in. Just an idea


@jkuczm suggested in the comments using Except[Integer[___], _Integer]. This seems to me to be the best option. It will match only atoms of head Integer without calling the evaluator.

$\endgroup$
4
  • $\begingroup$ Quite thorough. +1 $\endgroup$
    – Mr.Wizard
    Aug 13, 2012 at 9:11
  • $\begingroup$ Really clarifying. +1 $\endgroup$ Aug 13, 2012 at 16:38
  • 1
    $\begingroup$ @Rojo There's also Except[Integer@___, _Integer] pattern which, as far as I know, is robust, and doesn't involve evaluator. $\endgroup$
    – jkuczm
    Jan 26, 2017 at 13:52
  • $\begingroup$ @jkuczm That seems right, a very nice solution. $\endgroup$
    – Rojo
    Feb 9, 2017 at 3:43
12
$\begingroup$

A key issue, I think, is that PatternTest must evaluate its argument to work, even if function holds its arguments. Thus, there can be a huge difference in the timing between the two. Here's an example:

pic := Plot3D[Sin[1/x] Cos[1/y], {x, -1, 1}, {y, -1, 1},
  PlotPoints -> 100];
SetAttributes[f1, HoldFirst];
SetAttributes[f2, HoldFirst];
f1[x_Integer] := Prime[x] - x;
f2[x_?IntegerQ] := Prime[x] - x;
f1[pic] // Timing
f2[pic] // Timing

{6.*10^-6, f1[pic]}

{1.43547, f2[pic]}

When unanticipated, this type of evaluation is sometimes called an evaluation leak. I would certainly use Pattern whenever possible, although PatternTest is more general.

$\endgroup$
0
9
$\begingroup$

I think this question have gotten very good answers, but I just want to add my two cents to the topic.

f[_sheep] is a declarative typing, if something says it's a sheep, then it's a sheep, even if it doesn't make sense or will cause problems like: sheep[wolf[]]- Something which doesn't declare itself to be a Sheep isn't a Sheep even if you would suspect that it could perfectly well be assumed to be a sheep like wholly[sheep[]].

f[_?DuckQ] is duck typing. If it walks like a duck and quacks like a duck, then it's a duck. So for example if we define DuckQ[dog[]]=True then dog[] is a Duck, and can be given to any function assuming arguments of the type Duck[]

Declarative typing will be more efficient, since you can deduce everything from the heads of the arguments. Duck typing is more flexible, and allows you to do type checking without explicit declaring the type of everything. A good example is matrixQ. You could explicitly state that your data is a matrix and define

f[_matrix]:=....
f[matrix[{1,0,0},{0,1,0},{0,0,1}]]

But most consider it nicer to just write out the structure as a list of lists, and use duck typing to check if it's a matrix.

An additional element of this is that declaritive typing as implimented in Mathematica does not allow type hiarchies, so You cannot define your function to accept animals f[_animal]:=... and expect it to work for sheep[] and duck[] but this can be handled easily by an appropriate "duck typed" definition f[_?animalQ]:=... with animalQ[_sheep]=True;animalQ[_duck]=True. Again showing the flexibility of defining boolean checks.

$\endgroup$
2
  • $\begingroup$ nice analogy :) $\endgroup$
    – acl
    Aug 13, 2012 at 20:33
  • $\begingroup$ @CHM ambiguous in what sense? Maybe it could be improved. $\endgroup$
    – jVincent
    Aug 13, 2012 at 20:44
7
$\begingroup$

I'll try to answer "What's the difference between the two definitions, and which one should be used for maximum efficiency, robustness and cleanliness?".

The PatternTest approach is significantly more general, since the test can be arbitrarily complicated. Thus, we may define a function only for positive arguments that are even:

ClearAll@f;
f[x_?(# > 0 && EvenQ[#] &)] := 5

f[-2]
f[2]
f[3]
(*
f[-2]
5
f[3]
*)

Conversely, the more general approach may be slower, eg

ClearAll[f, g];
f[x_?IntegerQ] := x
g[x_Integer] := x

f[5] // timeIt
g[5] // timeIt

Mathematica graphics

where timeIt is

ClearAll[timeIt];
SetAttributes[timeIt, HoldAll]
timeIt[expr_] := Module[{t = Timing[expr;][[1]], tries = 1},
    While[t < 1.,
    tries *= 2;
    t = AbsoluteTiming[Do[expr, {tries}];][[1]];
    ];
    Return[t/tries]]
$\endgroup$
2
  • $\begingroup$ do you know why timeIt /@ {f[5], g[5]} gives different answers vs. {timeIt[f[5]], timeIt[g[5]]} ? thanks $\endgroup$
    – Nasser
    Aug 13, 2012 at 3:15
  • $\begingroup$ @NasserM.Abbasi timeIt is HoldAll. In your map version, f[5] and g[5] get evaluated before timeIt enters the game, because Map doesn't have a holding attribute. Try timeIt/@Unevaluated@{f[5], g[5]} $\endgroup$
    – Rojo
    Aug 13, 2012 at 11:07

Your Answer

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

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