How to check the validity of an option value

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.


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.


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] *)