16
$\begingroup$

Given that I have a function sqrt[x] that I must check the style and number of argument x.

I can do like this:

sqrt::badarg = "The argument `1` is not greater than or equal to zero.";
sqrt[x_] /;
 If[TrueQ[x >= 0], True, Message[sqrt::badarg, x]; False] := Sqrt[x]

Test1

sqrt[-1]
sqrt::badarg: The argument -1 is not greater than or equal to zero.
sqrt[1]
 1
sqrt[-1,2]
 sqrt[-1,2]

Now I have a function func which has three arguments sample1,sample2 and c.

The definition of func shown as belew:

In this case, I used the If[] to check the arguments step by step.

func::arrayerr = "`1` or `2` must be a valid 3D-array.";
func::badpara = "`1` must be a valid real number in the interval (0,1].";

func[sample1_, sample2_, c_] :=
 Module[{var1, var2},
  (*Check the argument that the user gives*)
  If[! (MatrixQ[sample1] && Length@First@sample1 == 3 &&
      MatrixQ[sample2] && Length@First@sample2 == 3),
    Message[func::arrayerr, sample1, sample2];
    Return[HoldForm@func[sample1, sample2, c]]
  ];
  If[! (0 < c <= 1),
    Message[func::badpara, c];
    Return[HoldForm@func[sample1, sample2, c]]
  ];
  (*main implementation*)
  c Join[sample1,sample2]
]

Test2

func[{{1, 2, 3}}, {{{1}, 2, 2, 3}}, 1]
func::arrayerr: {{1,2,3}} or {{{1},2,2,3}} must be a valid 3D-array.
func[{{1, 2, 3}}, {{4, 5, 6}, {7, 7, 2}}, 1]
{{1, 2, 3}, {4, 5, 6}, {7, 7, 2}}
func[{{1, 2}}, {{2, 2, 3, 4}}, 2]
func::arrayerr: {{1,2}} or {{2,2,3,4}} must be a valid 3D-array.

Question

  • Is it possible to know that how the Mathematica check the style and number of arguments for built-in functions ?

  • When I input func[{{1, 2}}, {{2, 2, 3, 4}}, 2, 4], how to give the warning information to user that the func only needs three arguments?

  • Is it appropriate for my If solution?

$\endgroup$
10
  • 2
  • $\begingroup$ I feel that I have already answered this question in (23345). If it were not for the bounty I might even close it. If my answer is not adequate or difficult to understand would you please explain why? $\endgroup$
    – Mr.Wizard
    Commented Jun 19, 2015 at 6:22
  • $\begingroup$ @Mr.Wizard, I am sorry. I didn't know your answer when I post this question. My question is that how the WRI deal with the warning information when they develop built-in functions. $\endgroup$
    – xyz
    Commented Jun 19, 2015 at 7:27
  • $\begingroup$ @Mr.Wizard Is there a uniform style to deal with warning informations? $\endgroup$
    – xyz
    Commented Jun 19, 2015 at 7:28
  • $\begingroup$ (1) The "catch-all method" seems to be the most commonly used internally and I gave three examples of that in my answer there. (2) Pardon me but I don't understand your question about "uniform style." $\endgroup$
    – Mr.Wizard
    Commented Jun 19, 2015 at 7:52

4 Answers 4

19
+500
$\begingroup$

I have moved the large addendum from my answer to How to program a F::argx message? to this post as I believe it is a better fit here. Please see that link for basic information before continuing.


Handling multiple messages with an auxiliary function

For full control of Message generation while retaining the canonical behavior of returning an unmatched expression an axillary function can be used to handle argument testing and message generation.

This axillary function can be used either:

  1. to check the arguments of the primary definition including correct arguments

  2. applied only to a fall-through definition for those that did not match the primary definition

In my opinion the former often has the advantage of less redundancy of the check patterns, while the latter has greater clarity and is easier to apply or retrofit.

A choice to be made is whether to issue only one error message or all messages that apply. Most System functions issue only one message if I recall correctly and your own example follows this apparent convention so I shall too.

Message generation needs to be a side-effect if the original expression is to be returned as is the case with built-in functions. Reference: Return the incomming expression on false

Fall-through method

Here is a simple example of the fall-through method. Both a General and user-created message are used for the sake of illustration. Note that to reduce redundancy I used no actual argument test in funcmsg but rather the problem is assumed based on the fact that the primary definition did not match. This may not be as robust as desired.

ClearAll[func, funcmsg]
Attributes[funcmsg] = {HoldAll};
func::"bad" = "Argument `1` is not a `2`.";

func[a : {__List}, i_: Automatic] := "operate"
_func?funcmsg := Null

funcmsg[f_[]] := Message[f::"argt", HoldForm[f], 0, 1, 2]
funcmsg[f_[_] | f_[_, _]] := Message[f::"bad", 1, "list of lists"]
funcmsg[x : f_[_, _, __]] := 
  Message[f::"argt", HoldForm[f], Length @ Unevaluated @ x, 1, 2]

Now any calls that do not match func[a : {__List}, i_: Automatic] (the primary definition) will be passed to the funcmsg function for messages to be issued. Examples, with echoed (returned) input expressly omitted:

func[]

func::argt: func called with 0 arguments; 1 or 2 arguments are expected. >>

func[1, 2]

func::bad: Argument 1 is not a list of lists.

func[{{1}, {2}}, 3]
"operate"

Applied to the specific example in your question:

ClearAll[func, funcmsg]
Attributes[funcmsg] = {HoldAll};

func::arrayerr = "`1` and `2` must be valid 3D arrays.";
func::badpara = "`1` must be a valid real number in the interval (0,1].";
func::arg3 = "func called with `1` arguments; 3 arguments are expected.";

mat3Q = MatrixQ[#] && Length@First@# == 3 &;

func[sample1_?mat3Q, sample2_?mat3Q, c_] /; 0 < c <= 1 := c Join[sample1, sample2]

_func?funcmsg := Null

funcmsg @ _[x___] /; Length@{x} != 3 := Message[func::arg3, Length@{x}]
funcmsg @ _[_, _, c_] /; ! 0 < c <= 1 := Message[func::badpara, c]
funcmsg @ _[a_, b_, _] /; ! (mat3Q[a] && mat3Q[b]) := Message[func::arrayerr, a, b]

Catch-all method

A variation of the method above to apply the message function in all calls rather than those that fail. This is used by internal functions such as ArrayPlot, BarChart, Histogram and PolarPlot. Examples:

e : ArrayPlot[args___] /; checkArgsOptions[e, 0, 1, hiddenOptions[ArrayPlot]] := 
 With[{caller = ArrayPlot, 
   caught = Catch[ArrayPlotInternal[False, getArgs[args], getOpts[args]]]}, 
  caught /; caught =!= $Failed]


BarChart[a : PatternSequence[___, Except[_?OptionQ]] | PatternSequence[], 
   o : OptionsPattern[]]?ChartArgCheck := 
 With[{res = Catch[iBarChart[BarChart, a, o], "ParseNoData" | "ChartingError", $Failed]}, 
  res /; Head[Unevaluated[res]] =!= $Failed]


Histogram[a : PatternSequence[___, Except[_?OptionQ]] | PatternSequence[], 
   o : OptionsPattern[]]?HistArgCheck := 
 With[{res = Catch[iHistogramLayer1[a, o], "ChartingError", $Failed]}, 
  res /; Head[Unevaluated[res]] =!= $Failed]

The definition of ChartArgCheck used by BarChart looks like this:

Attributes[ChartArgCheck] = {HoldAll}

ChartArgCheck[b : f_[{}, opts : OptionsPattern[]]] := True

ChartArgCheck[b : f_[{{} ..}, opts : OptionsPattern[]]] := True

ChartArgCheck[b : f_[args___, opts : OptionsPattern[]]] := 
 Block[{len}, len = Length[Unevaluated[{args}]];
   If[len <= 1, ArgumentCountQ[f, len, 1, 1], 
    Message[f::"nonopt", Last[Function[z, "HoldForm"[z], HoldAll] /@ Unevaluated[{args}]],
      1, "HoldForm"[b]];
    False, False]] && optCheck[b]

Note that in the third definition a Message may be produced, and that it additionally calls optCheck which may also produce a Message from its definition:

Attributes[optCheck] = {HoldAll}

optCheck[b : f_[args___, opts : OptionsPattern[]]] := 
 Module[{bad, good}, good = Join[Options[f], HiddenOptions[f]];
  bad = FilterRules[{opts}, Except[good]];
  If[Length[bad] > 0, Message[f::"optx", First[bad], "HoldForm"[b]];
   False, True, True]]

Note that in each case (ArrayPlot, BarChart, Histogram) the arguments are passed to an inner function that does that actual processing. This often is necessary to catch all the argument patterns desired while also handling correct arguments properly.

Here is a simplified, self-contained example:

ClearAll[foo, bar, check]

Attributes[check] = {HoldAll};
foo::"bad" = "Argument `1` is not a `2`.";

foo[args___]?check := bar[args]
bar[a_, i_: Automatic] := "operate"

check[f_[]] := Message[f::"argt", HoldForm[f], 0, 1, 2]

check[f_[Except[{__List}], ___]] := Message[f::"bad", 1, "list of lists"]

check[x : f_[___]] := With[{argln = Length @ Unevaluated @ x},
   If[argln < 3, True, Message[f::"argt", HoldForm[f], argln, 1, 2]]
 ]

Or applied to the specific example in your question:

ClearAll[func, main, check]
Attributes[check] = {HoldAll};

func::arrayerr = "`1` and `2` must be valid 3D arrays.";
func::badpara = "`1` must be a valid real number in the interval (0,1].";
func::arg3 = "func called with `1` arguments; 3 arguments are expected.";

mat3Q = MatrixQ[#] && Length @ First @ # == 3 &;

func[args___]?check := main[args]
main[sample1_, sample2_, c_] := c Join[sample1, sample2]

check @ _[x___] /; Length@{x} != 3 := Message[func::arg3, Length@{x}]
check @ _[_, _, c_] /; ! 0 < c <= 1 := Message[func::badpara, c]
check @ _[a_, b_, _] /; ! (mat3Q[a] && mat3Q[b]) := Message[func::arrayerr, a, b]
check @ else_ := True

Message generation within argument testing

Though less controllable than the auxiliary function methods another useful option is to perform message generation directly from within argument tests. It can be cleaner and faster as it reduces redundant testing but it may obfuscate simple tests and patterns, and message generation from complicated definitions may be somewhat unpredictable as a given test may fail (and generate a message) multiple times.

For this example I shall also use the additional streamlining of Macros`SetArgumentCount as described in How to program a F::argx message?.

func::arrayerr = "`1` must be a valid 3D array.";
func::badpara = "`1` must be a valid real number in the interval (0,1].";

Clear[func]

Macros`SetArgumentCount[func, 3]

mat3Q = MatrixQ[#] && Length@First@# == 3 || Message[func::arrayerr, #] &;
int01Q = 0 < # <= 1 || Message[func::badpara, #] &;

func[sample1_?mat3Q, sample2_?mat3Q, c_?int01Q] := c Join[sample1, sample2]
$\endgroup$
3
  • $\begingroup$ Dear Mr.Wizard, Today I encounter a new question about checking the value of option. For instance, NSolve[x^5 + 2 x + 7 == 0, x, WorkingPrecision -> "a"] gives the warning information NSolve::precbd: Requested precision a is not a machine-sized real number between $MinPrecision and $MaxPrecision. >>. I'd like to how to do this like NSolve $\endgroup$
    – xyz
    Commented Jun 28, 2015 at 6:22
  • $\begingroup$ @ShutaoTang Good question. Let me think about that, then update my answer. $\endgroup$
    – Mr.Wizard
    Commented Jun 28, 2015 at 6:50
  • $\begingroup$ Please [email protected] here $\endgroup$
    – xyz
    Commented Jun 28, 2015 at 6:54
6
$\begingroup$

It is often not necessary to use If to check arguments. Rather, since the formal arguments that appear in function definitions are almost always patterns to be matched, you can take advantage of Mathematica powerful pattern matching capabilities.

Here is a fairly simple example.

validColor = (_RGBColor | _GrayLevel | _Hue);

colorToRGB::badarg = 
  "bad color directive `1`; color set to black";

colorToRGB[c : validColor] :=
  (If[! And @@ ((0. <= # <= 1.) & /@ #) & @ c, 
     Message[colorToRGB::badarg, Style[Defer @ {c}, "SBO"]];
     Return @ RGBColor[0., 0., 0.]];
   Switch[c,
    _RGBColor, N @ c,
    _GrayLevel, ColorConvert[c, "RGB"],
    _Hue, ColorConvert[c, "RGB"]])

colorToRGB[args : ___] := 
  (Message[colorToRGB::badarg, Style[Defer @ {args}, "SBO"]];
   RGBColor[0., 0., 0.])

Note that, because even an argument matching the validColor pattern needs range checking, an If expression is used in the body of the first definition of colorToRGB. However, all other argument errors are caught by the second definition in which pattern matching does all the work.

You can find more about this style of defensive programming in this answer to a previously asked question.

$\endgroup$
3
+100
$\begingroup$

Your Which version might be improved. In general built-in functions do not return $Failed, but themselves, so, to do it, you usually need to use Conditional expressions. This might be written in many more forms, for example, reworking a little your own answer:

func::arrayerr = "`1` or `2` must be a valid 3D-array.";
func::badpara = 
  "`1` must be a valid real number in the interval (0,1].";
func::argnums = 
  "The func called `1` arguments, 3 arguments are needed.";
func[args___] /; 
   Length[{args}] =!= 3 := (Message[func::argnums, 
    Length[{args}]]; $Failed /; False);
func[sample1_, sample2_, c_] := 
 Module[{good = False, result}, 
  Which[! (MatrixQ[sample1] && Length@First@sample1 == 3 && 
       MatrixQ[sample2] && Length@First@sample2 == 3),
    Message[func::arrayerr, sample1, sample2];
    ,
    ! (0 < c <= 1),
    Message[func::badpara, c];
    ,
    good = True,
    result = c Join[sample1, sample2];
    ];
  result /; good
  ]

enter image description here

That will give you your desired result. You can also move your check of arguments number to the main downvalue definition:

func[args___] := 
 Module[{good = False, result, sample1, sample2, c}, 
  Which[
   Length[{args}] =!= 3,
   Message[func::argnums, Length[{args}]];
   ,
   {sample1, sample2, c} = {args};
   ! (MatrixQ[sample1] && Length@First@sample1 == 3 && 
      MatrixQ[sample2] && Length@First@sample2 == 3),
   Message[func::arrayerr, sample1, sample2];
   ,
   ! (0 < c <= 1),
   Message[func::badpara, c];
   ,
   good = True,
   result = c Join[sample1, sample2];
   ];
  result /; good
  ]

When you use multiple downvalues, you have to be sure they have different arguments patterns, or multiple definitions might be called if they return Conditional expressions evaluating to False.

$\endgroup$
2
$\begingroup$

Another way to go might be:

ClearAll[func]
func::arrayerr="`1` or `2` must be a valid 3D-array.";
func::badpara="`1` must be a valid real number in the interval (0,1].";

func[_,_,c_] := Message[func::badpara,c] /; c<=0||c>1 ;

func[a_,b_,_] := Message[func::arrayerr,a,b] /;
   !MatchQ[Dimensions[b],{_,3}] && !MatchQ[Dimensions[b],{_,3}];

func[sample1_,sample2_,c_] := c Join[sample1,sample2];

enter image description here

In your example, this clearly separates bad conditions from the final definition which may be clearer, since all the conditions are gone.

$\endgroup$
3
  • $\begingroup$ +1, Thanks. It is very elegant to replace MatrixQ[sample1] && Length@First@sample1 == 3 with MatchQ[Dimensions[b],{_,3}] $\endgroup$
    – xyz
    Commented Jun 19, 2015 at 5:06
  • $\begingroup$ @ShutaoTang Strictly speaking the operations are not the same. For example MatchQ[Dimensions[{{1, 2, 3}, {{4}, 5, 6}}], {_, 3}] is True yet MatrixQ[{{1, 2, 3}, {{4}, 5, 6}}] is False. $\endgroup$
    – Mr.Wizard
    Commented Jun 22, 2015 at 9:14
  • $\begingroup$ @Mr.Wizard, +1, $\endgroup$
    – xyz
    Commented Jun 22, 2015 at 9:28

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.