16

I have a user-defined function, func, which takes the following form:

func[arg1, arg2, options]

Here is the actual code

Options[func] = 
 {opt1 -> Automatic, opt2 -> False, opt3 -> {1, 0, 0}, 
  opt4 -> {0, 0, 0}};


func::invarg1 = "`1` should be a numerical array construction with dimention 2.";
func::invarg2 = "`1` should be a postive value.";

func::invopt1 = 
 "Value of option opt1 \[Rule] `1` should be a valid value like Automatic or deCasteljau.";
func::invopt2 = 
  "Value of option opt2 \[Rule] `1` should be a valid boolean value like True or False.";
func::invopt3 = 
 "Value of option opt3 \[Rule] `1` should be a non-negative machine-sized 
  number list of length 3.";
func::invopt4 = 
 "Value of option opt4 \[Rule] `1` should be a non-negative machine-sized 
  number list of length 3.";

SyntaxInformation[func] = {"ArgumentsPattern" -> {_, _, OptionsPattern[]}};

func[arg1_, arg2_, opts : OptionsPattern[]] /; 
  MatrixQ[arg1, NumericQ] && arg2 > 0 :=
  Module[{o1, o2, o3, o4},
   o1 = OptionValue[opt1];
   o2 = OptionValue[opt2];
   o3 = OptionValue[opt3];
   o4 = OptionValue[opt4];
   If[! MemberQ[{Automatic, "deCasteljau"}, o1],
    Message[func::invopt1, o1];
    Return[$Failed]
   ];
   If[! MemberQ[{True, False}, o2],
    Message[func::invopt2, o2];
    Return[$Failed]
   ];
   If[! (VectorQ[o3, NumericQ] && Length@o3 == 3),
    Message[func::invopt3, o3];
    Return[$Failed]
   ];
   If[! (VectorQ[o4, NumericQ] && Length@o4 == 3),
    Message[func::invopt4, o4];
    Return[$Failed]
   ];
   (*continue to do something*)
   {arg1, arg2, o1, o2, o3, o4}
 ]

For the arguments arg1,arg2, a better checking is via the side-effect, rather than If[] statement.

func[arg1_ /; ! MatrixQ[arg1, NumericQ], arg2_, opts : OptionsPattern[]] /; 
  (Message[func::invarg1, arg1]; False) :=  $Failed
func[arg1_, arg2_ /; arg2 <= 0, opts : OptionsPattern[]] /; 
  (Message[func::invarg2, arg2]; False) := $Failed

In implementation of func, I use If to check the validity of the options step by step. Nevertheless, I believe this is not an ideal solution in Mathematica, especially when a function has many ($30$ or more) options.

So I would like to know:

  • Which method is ideaL/better to deal with the vality of options?
Mr.Wizard
  • 271,378
  • 34
  • 587
  • 1,371
xyz
  • 605
  • 4
  • 38
  • 117

3 Answers3

15

Implementation

This is indeed an important problem. It is usually best to have a separate function testing various options. Here is the solution I propose: a wrapper that would factor out the testing functionality from the main function. Here is the code:

ClearAll[OptionCheck];
OptionCheck::invldopt = "Option `1` for function `2` received invalid value `3`";
OptionCheck[testFunction_]:=
  Function[code, 
    Module[{tag, msg, catch},
      msg = Function[{v, t},Message[OptionCheck::invldopt, Sequence @@ t]; v];
      catch = Function[c, Catch[c, _tag, msg], HoldAll]; 
      catch @ ReplaceAll[
        Unevaluated @ code, 
        o:HoldPattern[OptionValue[f_,_,name_]]:> With[{val = o},
          If[!testFunction[name, val], 
            Throw[$Failed, tag[name, f, val]],
            (* else *)
            val
          ]
        ]
      ]
    ],
    HoldAll
];

Examples

Let me show how to use it: first comes a function to test options

ClearAll[test];
test["a", val_] := IntegerQ[val];
test["b", val_] := MatchQ[val, True | False];

Here is the main function we want to implement:

ClearAll[f];
Options[f] = {"a" -> 1, "b" -> False};
f[x_, y_, OptionsPattern[]] := 
  OptionCheck[test]@ Module[{z = x + y, q, a},
    a = OptionValue["a"];
    q = If[OptionValue["b"], a, a + 1];
    {x, y, q}
  ]

The entire testing code is now factored out into the OptionCheck[test] block. Let's see how it works:

f[1, 2]
f[1, 2, "a" -> 10]
f[1, 2, "a" -> 10, "b" -> False]

(* 
  {1, 2, 2}
  {1, 2, 11}
  {1, 2, 11}
*)

So far so good, the options passed were valid. Now let us pass some invalid options:

f[1, 2, "a" -> 1.5]

During evaluation of In[806]:= OptionCheck::invldopt: Option a for function f received invalid value 1.5`

(* $Failed *)

f[1, 2, "a" -> 1, "b" -> 2]

During evaluation of In[807]:= OptionCheck::invldopt: Option b for function f received invalid value 2

(* $Failed  *)

How it works

I used the fact that OptionValue is a magical function, which expands from OptionValue[name] to OptionValue[f, opts, name] before the code of the r.h.s. of the function evaluates. So, by the time the OptionCheck executes, all entries of OptionValue have been expanded. So we can analyze the code and wrap OptionValue[f, opts, name] into testing code, and then execute it. So, OptionCheck is an example of applied metaprogramming.

Leonid Shifrin
  • 114,335
  • 15
  • 329
  • 420
6

Preamble

Leonid's method is new to me and quite interesting. I expect that as with most of his methods it is well reasoned and has advantages that are not immediately apparent. Nevertheless I also find value in alternative methods, so here is one of mine. I shall use his example code so that these methods may be compared directly.

Boilerplate

General::invldopt = "Option `2` for function `1` received invalid value `3`";

optsMsg[f_][op_, val_] :=
  test[f, op][val] || Message[General::invldopt, f, op, val]

Attributes[optsCheck] = {HoldFirst};

optsCheck @ head_[___, opts : OptionsPattern[]] :=
  And @@ optsMsg[f] @@@ FilterRules[{opts}, Options @ head]

Function code

ClearAll[f, test];

Options[f] = {"a" -> 1, "b" -> False};

test[f, "a"] := IntegerQ;
test[f, "b"] := BooleanQ;

f[x_, y_, OptionsPattern[]]?optsCheck :=
  Module[{z = x + y, q, a},
    a = OptionValue["a"];
    q = If[OptionValue["b"], a, a + 1];
    {x, y, q}
  ]

Testing

Correct input works as expected:

f[1, 2]                              (* out=  {1, 2, 2}  *)
f[1, 2, "a" -> 10]                   (* out=  {1, 2, 11} *)
f[1, 2, "a" -> 10, "b" -> False]     (* out=  {1, 2, 11} *)

Unlike Leonid I chose a method that:

  1. issues messages for multiple incorrect option values

  2. returns the original input unevaluated

Example:

f[1, 2, "a" -> 1.5, "b" -> 2]

General::invldopt: Option a for function f received invalid value 1.5`

General::invldopt: Option b for function f received invalid value 2

f[1, 2, "a" -> 1.5, "b" -> 2]

Note:

  • I did not account for held option values in the writing of this code as I do not believe Leonid did in his. However I think it would be easy to change optsMsg to address that, if requested.

  • In the code above I did a ClearAll on test to avoid any collision with Leonid's code. However in my implementation test would be shared between all functions using optsCheck and should therefore not be cleared. It should probably also have a more descriptive name but again I followed Leonid's example for the sake of comparison.

Mr.Wizard
  • 271,378
  • 34
  • 587
  • 1,371
  • @ShutaoTANG Yes, the Messages are generated as a side-effect; optsCheck will only return True if none are generated. You could certainly issue different messages; I left that out only because I thought it was not the focus of the question, and because Leonid did not include it. We have discussed message generation before; where do you see difficulty in implementing them here? – Mr.Wizard Jun 05 '16 at 04:21
  • 1
    Nice and simple, +1. The main difference is that, arguably, all methods which check options at the point of entry, so to speak (yours among them), are somewhat more intruding than mine, because, first, they recompute the options (keep in mind that they can be passed with RuleDelayed), and second, they check all options, while in the code some of them may not be reached at all, if they happen to be on false branches of If and such. My method only checks those options that are actually computed during the code execution. So, I think yours is cleaner, but mine less intruding. – Leonid Shifrin Jun 05 '16 at 10:24
  • 2
    To me, your method looks closer in spirit to the option-testing framework of jkuczm. His method uses definition-time macro-expansion + patterns, yours uses testing via patterns (so, normal functions), mine uses run-time macro-expansion - these are technical differences. It's great to have all these options, I think. – Leonid Shifrin Jun 05 '16 at 10:26
  • @LeonidShifrin I have little practical experience with this because my programs are not large, but I can see the advantage in your method. It makes me think perhaps a language extension would be appropriate, e.g. a OptionValueTest function that works without the need for the OptionCheck[test] wrapper, and references a TestValues list associated with the head. – Mr.Wizard Jun 05 '16 at 19:25
  • @Mr.Wizard I agree that such an extension would've been useful, but it would somehow need some mechanism to register specific test functions, which would be natural and readable. So may be, some version of external framework based on some of the ideas / implementations we came up with here, could still be quite useful. – Leonid Shifrin Jun 05 '16 at 20:04
  • @Leonid A counter-argument comes to mind: it may be desirable to check all option values for validity before proceeding with a potentially costly body evaluation. In such a case if one waits to check the value until time of use significant calculation may be needlessly wasted. As noted I did not account for RuleDelayed options at this point, and that is something to consider. Conceptually I like the idea of checking arguments of all types at the same time, before the evaluation of the body of the definition, rather than checking some first, and others later. (continued) – Mr.Wizard Jun 05 '16 at 20:07
  • This notwithstanding there would of course be cases where delayed checking is desirable and your method handles that with a very nice abstraction that is new to me. As you said I am glad we have choices. – Mr.Wizard Jun 05 '16 at 20:08
  • @Mr.Wizard Yes, sure, I agree. Which method to use should mostly depend in the problem. Yours is very nice, and in fact something similar is often done in the internal code I've seen. – Leonid Shifrin Jun 05 '16 at 20:29
6

You can use my OptionsValidation framework to add options validation to your functions.

We start by loading the package:

Import["https://raw.githubusercontent.com/jkuczm/MathematicaOptionsValidation/master/NoInstall.m"]

Now "register" tests you want to perform on option values. You do it by defining CheckOption for your function.

ClearAll[func]
Options[func] =
    {opt1 -> Automatic, opt2 -> True, opt3 -> {1, 2, 3}, opt4 -> {4, 5, 6}};

func::optAutDeCast =
    "Value of option `1` -> `2` should be Automatic or deCasteljau.";
func::opt3NonNegNum = 
  "Value of option `1` -> `2` should be list of three non-negative numbers.";

CheckOption[func, opt1][val : Except[Automatic | "deCasteljau"]] := 
    Message[func::optAutDeCast, opt1, HoldForm@val]
CheckOption[func, opt2][val : Except[True | False]] := 
    Message[func::opttf, opt2, HoldForm@val]
CheckOption[func, opt : opt3 | opt4][
    val : Except[{Repeated[(_?NumberQ)?NonNegative, {3}]}]
] := 
    Message[func::opt3NonNegNum, opt, HoldForm@val]

SetDefaultOptionsValidation[func];

Once tests are "registered" you can use various strategies to use tests in your function definition. Here I'll show two strategies - reproducing already given answers.

Leonid's answer

You can use WithOptionValueChecks environment to "decorate" all OptionValue calls performed in body of your function. Each call of OptionValue inside WithOptionValueChecks environment is accompanied by appropriate test, if test fails - function body evaluation stops and $Failed is returned.

This approach tests only values of those options that are actually used while evaluating body of function, but it tests them regardless of whether they were actually passed to function, or taken from defaults.

DownValues[func] = {};
func[arg1_, arg2_, OptionsPattern[]] :=
    WithOptionValueChecks@Module[{o1, o2, o3, o4},
        {o1, o2, o3, o4} = OptionValue[{opt1, opt2, opt3, opt4}];
        (* Do something. *)
        {arg1, arg2, o1, o2, o3, o4}
    ]

Function called with valid option values:

func[a, b, opt3 -> {12, 3, 0}, opt1 -> "deCasteljau"]
(* {a, b, "deCasteljau", True, {12, 3, 0}, {4, 5, 6}} *)

and with invalid value:

func[a, b, opt1 -> "wrongValue"]
(* func::optAutDeCast: Value of option opt1 -> wrongValue should be Automatic or deCasteljau. *)
(* $Failed *)

Mr.Wizard's answer

You can use ValidOptionsPattern instead of OptionsPattern to perform tests in pattern matching phase, before evaluation of function body starts. If all given options are valid - pattern will match, otherwise - function will remain unevaluated.

With this approach each function call will test only those options that were actually explicitly given to function. Testing of default values is performed only when they are changed using SetOptions.

DownValues[func] = {};
func[arg1_, arg2_, ValidOptionsPattern[func]] :=
    Module[{o1, o2, o3, o4},
        {o1, o2, o3, o4} = OptionValue[{opt1, opt2, opt3, opt4}];
        (* Do something. *)
        {arg1, arg2, o1, o2, o3, o4}
    ]

Call function with valid options:

func[a, b, opt2 -> False, opt4 -> {9, 1, 7}]
(* {a, b, Automatic, False, {1, 2, 3}, {9, 1, 7}} *)

Call function with invalid option value and with unknown option:

func[a, b, opt2 -> "nonBoolean", opt3 -> {-2, 1, x}, wrongOptionName -> value]
(* func::opttf: Value of option opt2 -> nonBoolean should be True or False. >> *)
(* func::opt3NonNegNum: Value of option opt3 -> {-2,1,x} should be list of three non-negative numbers. *)
(* CheckOption::optnf: wrongOptionName is not a known option for func. *)
(* func[a, b, opt2 -> "nonBoolean", opt3 -> {-2, 1, x}, wrongOptionName -> value] *)
jkuczm
  • 15,078
  • 2
  • 53
  • 84