Specifying settings for Options
How do you explicitly state the valid settings for an option? Take this example for in开发者_如何转开发stance
Options[myFunc] = {opt1 -> "SomeString"};
myFunc[OptionsPattern[]] := Print[OptionValue[opt1]];
myFunc
prints the value of the option. If we evaluate myFunc[opt1 -> {1, 2}]
then it prints {1, 2}
. This function will essentially print anything that you set to opt1
. My question is, how can I make sure that my function only accept a given number of values for opt1
. We can start with something simple like a String
and an Integer
.
To have a better idea of the behavior that we would expect when given the wrong values for opt1
we can take a look at what happens when we give the wrong values for PlotRange
in the function Plot
.
In the example given in the picture I purposely gave wrong values to the PlotRange
options and gave me a message specifying the correct type of values for that particular options. It seems that it PlotRange
ended up taking its default value and thus it return the Graphics
object.
In the simple example what we would like to obtain is something like:
myFunc::sometag : Value of option opt1 -> `1` is not a string or integer.
How to achieve this?
A straightforward solution
Here is a simple way:
In[304]:= ClearAll[myFunc];
Options[myFunc] = {opt1 -> "SomeString"};
myFunc::badopt = "Value of option opt1 -> `1` is not a string or integer.";
myFunc[OptionsPattern[]] :=
With[{val = OptionValue[opt1]},
With[{error = ! MatchQ[val, _String | _Integer]},
If[error, Message[myFunc::badopt , val]];
(Print[val] /; ! error)]];
For example:
In[308]:= myFunc[opt1 -> 1]
During evaluation of In[308]:= 1
In[309]:= myFunc[opt1 -> {1, 2}]
During evaluation of In[309]:= myFunc::badopt:
Value of option opt1 -> {1,2} is not a string or integer.
Out[309]= myFunc[opt1 -> {1, 2}]
Making it general with custom assignment operators
We can use the fact that OptionValue
inside a function works with a single argument being an option name, to factor out the error-checking tedium. This is possible by using mma meta-programming facilities. Here is the code for a custom assignment operator:
ClearAll[def, OptionSpecs];
SetAttributes[def, HoldAll];
def[f_[args___] :> body_,OptionSpecs[optionSpecs : {(_ -> {_, Fail :> _}) ..}]] :=
f[args] :=
Module[{error = False},
Scan[
With[{optptrn = First[# /. optionSpecs], optval = OptionValue[#]},
If[! MatchQ[optval, optptrn ],
error = True;
Return[(Fail /. Last[# /. optionSpecs])[optval]]]] &,
optionSpecs[[All, 1]]
];
body /; ! error];
What it does is to take a function definition as a rule f_[args___]:>body_
, and also
the specifications for the acceptable options settings and actions to perform upon detection of an error in one of the passed options. We then inject the error-testing code (Scan
) before the body gets executed. As soon as the first option with inappropriate setting is found, error flag is set to True
, and whatever code is specified in the Fail:>code_
part of specifications for that option. The option specification pattern (_ -> {_, Fail :> _})
should read (optname_ -> {optpattern_, Fail :> onerror_})
, where optname
is an option name, optpattern
is a pattern that the option value must match, and onerror
is arbitrary code to execute if error is detected. Note that we use RuleDelayed
in Fail:>onerror_
, to prevent premature evaluation of that code. Note b.t.w. that the OptionSpecs
wrapper was added solely for readability - it is a completely idle symbol with no rules attached to it.
Here is an example of a function defined with this custom assignment operator:
ClearAll[myFunc1];
Options[myFunc1] = {opt1 -> "SomeString", opt2 -> 0};
myFunc1::badopt1 = "Value of option opt1 -> `1` is not a string or integer.";
myFunc1::badopt2 = "Value of option opt2 -> `1` is not an integer.";
def[myFunc1[OptionsPattern[]] :>
Print[{OptionValue[opt1], OptionValue[opt2]}],
OptionSpecs[{
opt1 -> {_Integer | _String,
Fail :> ((Message[myFunc1::badopt1, #]; Return[$Failed]) &)},
opt2 -> {_Integer,
Fail :> ((Message[myFunc1::badopt2, #]; Return[$Failed]) &)}}
]];
Here are examples of use:
In[473]:= myFunc1[]
During evaluation of In[473]:= {SomeString,0}
In[474]:= myFunc1[opt2-> 10]
During evaluation of In[474]:= {SomeString,10}
In[475]:= myFunc1[opt2-> 10,opt1-> "other"]
During evaluation of In[475]:= {other,10}
In[476]:= myFunc1[opt2-> 1/2]
During evaluation of In[476]:= myFunc1::badopt2:
Value of option opt2 -> 1/2 is not an integer.
Out[476]= $Failed
In[477]:= myFunc1[opt2-> 15,opt1->1/2]
During evaluation of In[477]:= myFunc1::badopt1:
Value of option opt1 -> 1/2 is not a string or integer.
Out[477]= $Failed
Adding option checks to already defined functions automatically
You might also be interested in a package I wrote to test the passed options: CheckOptions
, available here. The package comes with a notebook illustrating its use. It parses the definitions of your function and creates additional definitions to check the options. The current downside (apart from generation of new definitions which may not always be appropriate) is that it only covers older way to define options through OptionQ
predicate (I did not yet update it to cover OptionValue - OptionsPattern
. I will reproduce here a part of the accompanying notebook to illustrate how it works:
Consider a model function:
In[276]:= ClearAll[f];
f[x_, opts___?OptionQ]:= x^2;
f[x_, y_, opts___?OptionQ] := x + y;
f[x_, y_, z_] := x*y*z;
Suppose we want to return an error message when an option FontSize
is passed to our function:
In[280]:=
f::badopt="Inappropriate option";
test[f,heldopts_Hold,heldArgs_Hold]:=(FontSize/.Flatten[List@@heldopts])=!=FontSize;
rhsF[f,__]:=(Message[f::badopt];$Failed);
We add the option - checking definitions:
In[283]:= AddOptionsCheck[f,test,rhsF]
Out[283]= {HoldPattern[f[x_,opts___?OptionQ]/;test[f,Hold[opts],Hold[x,opts]]]:>
rhsF[f,Hold[opts],Hold[x,opts]],
HoldPattern[f[x_,y_,opts___?OptionQ]/;test[f,Hold[opts],Hold[x,y,opts]]]:>
rhsF[f,Hold[opts],Hold[x,y,opts]],
HoldPattern[f[x_,opts___?OptionQ]]:>x^2,
HoldPattern[f[x_,y_,opts___?OptionQ]]:>x+y,
HoldPattern[f[x_,y_,z_]]:>x y z}
As you can see, once we call AddOptionsCheck
, it generates new definitions. It takes the function name, the testing function, and the function to execute on failure. The testing function accepts the main function name, options passed to it (wrapped in Hold
), and non-options arguments passed to it (also wrapped in Hold
). From the generated definitions you can see what is does.
We now check on various inputs:
In[284]:= f[3]
Out[284]= 9
In[285]:= f[3,FontWeight->Bold]
Out[285]= 9
In[286]:= f[3,FontWeight->Bold,FontSize->5]
During evaluation of In[286]:= f::badopt: Inappropriate option
Out[286]= $Failed
In[289]:= f[a,b]
Out[289]= a+b
In[290]:= f[a,b,FontWeight->Bold]
Out[290]= a+b
In[291]:= f[a,b,FontWeight->Bold,FontSize->5]
During evaluation of In[291]:= f::badopt: Inappropriate option
Out[291]= $Failed
In[292]:= OptionIsChecked[f,test]
Out[292]= True
Please note that the test function can test for arbitrary condition involving function name, passed arguments and passed options. There is another package of mine, PackageOptionChecks
, available at the same page, which has a simpler syntax to test specifically r.h.s. of options, and can also be applied to entire package. A practical example of its use is yet another package, PackageSymbolsDependencies
, whose functions' options are "protected" by PackageOptionChecks
. Also, PackageOptionChecks
may be applied to functions in Global'
context as well, it is not necessary to have a package.
One other limitation of the current implementation is that we can not return the function unevaluated. Please see a more detailed discussion in the notebook accompanying the package. If there is enough interest in this, I will consider updating the package to remove some of the limitations I mentioned.
精彩评论