How to check the style and number of arguments like the built-in functions?

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]

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.


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.