16
$\begingroup$

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?
$\endgroup$
4
  • 3
    $\begingroup$ Related: Manipulations with options $\endgroup$
    – jkuczm
    Jun 4, 2016 at 12:34
  • $\begingroup$ @xyz Thanks a lot for the bounty, but to what do I owe such a generous gift? Particularly when we consider how big a fraction it is of your current rep points? $\endgroup$ Mar 9, 2020 at 12:14
  • $\begingroup$ @LeonidShifrin I have left stack exchange for about 3 years and do not use Mathematica any more. Recently I notice that the rep is 4K, so I think it should be given to the people that even help me. So thanks for your answer. $\endgroup$
    – xyz
    Mar 9, 2020 at 12:23
  • $\begingroup$ I see. I was expecting something like that :( Well, thanks again, and good luck with whatever you are up to now! $\endgroup$ Mar 9, 2020 at 17:07

3 Answers 3

15
+500
$\begingroup$

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.

$\endgroup$
3
  • $\begingroup$ @ShutaoTANG Was glad to help. Re: metaprogramming - you can start with this Wikipedia article. In the context of Mathematica, the relevant discussion is here. $\endgroup$ Jun 4, 2016 at 11:36
  • $\begingroup$ @Leonid I took the liberty to steal your approach and incorporate it into new version of OptionsValidation framework. $\endgroup$
    – jkuczm
    Jul 4, 2016 at 12:58
  • $\begingroup$ @jkuczm Sure, that's great. That's an impressive body of work, b.t.w. Unfortunately, I don't have the time right now to look closer at the code, but it looks quite monumental. Will put it on my todo list to have a closer look at it. $\endgroup$ Jul 4, 2016 at 13:18
6
$\begingroup$

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.

$\endgroup$
8
  • $\begingroup$ @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? $\endgroup$
    – Mr.Wizard
    Jun 5, 2016 at 4:21
  • 1
    $\begingroup$ 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. $\endgroup$ Jun 5, 2016 at 10:24
  • 2
    $\begingroup$ 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. $\endgroup$ Jun 5, 2016 at 10:26
  • $\begingroup$ @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. $\endgroup$
    – Mr.Wizard
    Jun 5, 2016 at 19:25
  • $\begingroup$ @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. $\endgroup$ Jun 5, 2016 at 20:04
6
$\begingroup$

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] *)
$\endgroup$

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.