OptionsValidation


Framework simplifying validation of options.

It provides functions registering arbitrary tests for values of options, of given symbols, with names matching given patterns. Test of relations between different options can be also registered.

Registered tests can be automatically used in various different strategies of option value testing. Tests can be performed while evaluating body of function when option values are accessed, or they can be performed upfront while matching function pattern. When tests fail - function can either return a value denoting failure, or can remain unevaluated.


To add options validation to a function we first need to "register" test for specific options by adding definitions of CheckOption function. There are two supported ways to define tests. We can use explicit pattern for option values: CheckOption[functionPattern, optNamePattern][optValuePattern] := ..., or assign a function testing option value: CheckOption[functionPattern, optNamePattern] := testFunction. If the latter version is used, and testFunction holds its first argument, it can test unevaluated values of options, given as delayed rules.

CheckOptionRelations[functionPattern] = ... can be used to "register" tests of relations between options.

Similarly to how defining Options[f] = ... really associates definitions with f, not with Options symbol, CheckOption... definitions will be assigned to appropriate functions given as first argument.

Once tests are registered we must decide how those tests should be used by our functions. Tests can be performed while evaluating body of function when option values are accessed, or they can be performed upfront while matching function pattern. When tests fail - function can either return a value denoting failure (e.g. $Failed), or can remain unevaluated.

Package provides also SetDefaultOptionsValidation function causing testing of default option values, of specific function, each time they are changed using SetOptions function.

Below we present four combinations of option validation strategies.

We start by loading the package directly from repository, without installation:

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

Function body "runtime" tests

Here we define simple function with one option accepting non-negative integer or infinity as value. We use WithOptionValueChecks environment that "decorates" OptionValue calls inside body of function. Each call of OptionValue inside WithOptionValueChecks environment is accompanied by appropriate test, if test fails - function body evaluation stops and $Failed is returned.

ClearAll[f]

Options[f] = {opt -> 100};

CheckOption[f, opt][val : Except[_Integer?NonNegative | Infinity]] :=
    Message[f::iopnf, opt, HoldForm@val]

SetDefaultOptionsValidation[f];

f[x_, OptionsPattern[]] := WithOptionValueChecks@{x, OptionValue[opt]}

Function called with valid option value:

f["str", opt -> 11]
(* {"str", 11} *)

and with invalid option value:

f[sym, "opt" -> -2]
(* f::iopnf: Value of option opt -> -2 should be a non-negative integer or Infinity. >> *)
(* $Failed *)

Attempt to set invalid default option value:

SetOptions[f, opt -> "nonInteger"]
(* f::iopnf: Value of option opt -> nonInteger should be a non-negative integer or Infinity. >> *)
(* $Failed *)

Pattern matching tests, function evaluates on invalid options, sub-option tests

If we want tests to be performed when matching function pattern, before function body evaluates, but we want function to evaluate to something, e.g. $Failed, when options are invalid, we can set first function definition to take InvalidOptionsPattern and most general non-option arguments pattern. Since all invalid options are taken care of by first definition, there's no need to test options validity in subsequent definitions so we can use ordinary OptionsPattern.

In this example our function will take one option with sub-options. One of sub-options accepts only boolean values. We define test only for sub-option, it'll be automatically used when testing parent option.

ClearAll[f]

Options[f] = {opt -> {subOpt1 -> True, subOpt2 -> "strOpt"}};

CheckOption[f, opt -> subOpt1][val : Except[True | False]] :=
    Message[f::opttf, HoldForm[opt -> subOpt1], HoldForm[val]]

SetDefaultOptionsValidation[f];

f[_Integer | _String, InvalidOptionsPattern[f]] := $Failed
f[i_Integer, OptionsPattern[]] :=
    {Integer, i, OptionValue[{opt -> subOpt1, opt -> subOpt2}]}
f[s_String, OptionsPattern[]] :=
    {String, s, OptionValue[{opt -> subOpt1, opt -> subOpt2}]}

Function called with valid option values:

f[5, opt -> `arbitraryContext`subOpt1 -> True]
(* {Integer, 5, {True, "strOpt"}} *)
f["str", "opt" -> {subOpt1 -> False, subOpt2 -> x}]
(* {String, "str", {False, x}} *)

with invalid option values:

f[-8, opt -> {subOpt2 -> 10, subOpt1 -> "strOpt"}]
(* f::opttf: Value of option opt->subOpt1 -> strOpt should be True or False. >> *)
(* $Failed *)
f["str", `arbitraryContext`opt -> "subOpt1" -> 10]
(* f::opttf: Value of option opt->subOpt1 -> 10 should be True or False. >> *)
(* $Failed *)

with unknown options:

f[2, unknown1 -> val1, unknown2 -> val2]
(* CheckOption::optnf: unknown1 is not a known option for f. *)
(* CheckOption::optnf: unknown2 is not a known option for f. *)
(* $Failed *)

Pattern matching tests, function remains unevaluated on invalid options, exclusive definitions, test unevaluated option value

If we want function to remain unevaluated when options are invalid, and function DownValues are exclusive, in the sense that each possible combination of non-option arguments can match function DownValues in only one way, then we can use ValidOptionsPattern in each down-value.

In this example function has one option, that accepts only expressions with head Plus. Option values are not evaluated during tests.

ClearAll[f]

Options[f] = {opt :> 2 + 2};

f::optPlus = "Value of option `1` -> `2` should be an expression with head Plus.";

CheckOption[f, opt] =
    Function[val,
        Head@Unevaluated[val] === Plus ||
            Message[f::optPlus, HoldForm@opt, HoldForm@val],
        HoldFirst
    ];

SetDefaultOptionsValidation[f];

f[i_Integer, ValidOptionsPattern[f]] :=
    {Integer, i, OptionValue[Automatic, Automatic, opt, Hold]}
f[s_String, ValidOptionsPattern[f]] :=
    {String, s, OptionValue[Automatic, Automatic, opt, Hold]}

Function called with valid option values:

f[5, opt :> 1 + 10]
(* {Integer, 5, Hold[1 + 10]} *)
f["str", `arbitraryContext`opt -> a + b]
(* {String, "str", Hold[a + b]} *)

with invalid option values:

f[5, "opt" -> 10]
(* f::optPlus: Value of option opt -> 10 should be an expression with head Plus. *)
(* f[5, "opt" -> 10] *)
f["str", opt :> 2 * 3]
(* f::optPlus: Value of option opt -> 2 * 3 should be an expression with head Plus. *)
(* f["str", opt :> 2 * 3] *)

with unknown option:

f["something", "unknownOptionName" -> "value"]
(* CheckOption::optnf: unknownOptionName is not a known option for f. *)
(* f["something", "unknownOptionName" -> "value"] *)

Pattern matching tests, function remains unevaluated on invalid options, non-exclusive definitions, test option relations

If we want function to remain unevaluated when options are invalid, but function DownValues are such that there exist combination of non-option arguments that can match DownValues in more than one way (this includes several definitions with overlapping non-option patterns, but also single definition with competing variable-length patterns), then we can use QuietValidOptionsPattern in each down-value and add last definition with most general non-option arguments and ValidOptionsPattern.

In this example function has two options, first accepts string or list of strings, second accepts an integer. This options are related in such way that value of second option should not be larger than sum of lengths of strings given in first option.

ClearAll[f]

Options[f] = {opt1 -> "default", opt2 -> 0};

CheckOption[f, opt1][val : Except[_String | {__String}]] :=
    Message[f::opstl, HoldForm@opt1, HoldForm@val]

CheckOption[f, opt2] =
    IntegerQ[#] || Message[f::modn, HoldForm@opt2, HoldForm@#]&;

f::optRel = "\
Value of option opt2 -> `2` shouldn't be larger than total length of strings \
given in option opt1 -> `1`.";

CheckOptionRelations[f] =
    Module[{val1, val2},
        {val1, val2} = OptionValue[f, #, {opt1, opt2}];
        val2 <= Quiet@StringLength@StringJoin[val1] ||
            Message[f::optRel, HoldForm@Evaluate@val1, HoldForm@Evaluate@val2]
    ]&;

SetDefaultOptionsValidation[f];

f[i_Integer, QuietValidOptionsPattern[f]] :=
    {Integer, i, OptionValue[{opt1, opt2}]}
f[x_, QuietValidOptionsPattern[f]] :=
    {"general", x, OptionValue[{opt1, opt2}]}
f[_, ValidOptionsPattern[f]] := "never reached"

Function called with valid option values:

f[5, opt1 -> "optStr", opt2 -> 3]
(* {Integer, 5, {"optStr", 3}} *)
f["str", `arbitraryContext`opt1 -> {"optStr1", "optStr2"}]
(* {"general", "str", {{"optStr1", "optStr2"}, 0}} *)

with one valid and one invalid option value, relation is valid:

f[5, opt1 -> "str", "opt2" -> 1.1]
(* f::modn: Value of option opt2 -> 1.1` should be an integer. >> *)
(* f[5, opt1 -> "str", "opt2" -> 1.1] *)

with option value that is individually valid, but violates relation:

f[x, opt2 -> 10]
(* f::optRel: Value of option opt2 -> 10 shouldn't be larger than total length of strings given in option opt1 -> default. *)
(* f[x, opt2 -> 10] *)