5
$\begingroup$

Algorithms can often be splitted into smaller and reusable functions (that's a good thing).

However with this scheme it is not clear to me how to ensure the consistency of the option values and their proper forwarding across functions.

Imagine that you have 2 functions: foo1 and foo2, where a,b are your algorithm parameters:

Options[foo1] = {a -> 1, b -> 2};

foo1[arg__, opts : OptionsPattern[]] := 
   Print["foo1 a=", OptionValue[a], " b=", OptionValue[b]]; 

Options[foo2] = {b -> -2};

foo2[arg__, opts : OptionsPattern[]] := 
   Print["foo2 b=", OptionValue[b]];

Now you want to define a foo3 function that reuses foo1 and foo2

Options[foo3] = {a -> 10, c -> 4};

foo3[opts : OptionsPattern[]] := Block[{},
   foo1[1, 2, FilterRules[{opts}, Options[foo1]]];
   foo2[4, 5, FilterRules[{opts}, Options[foo2]]];
   ];

Running this example:

foo3[]

prints:

foo1 a=1 b=2

foo2 b=-2

IMHO there are two main issues:

  • the a->10 default value is not forwarded
  • incompatible option b->2 for foo1 and b->-2 for foo2 is not detected

Another problem is that OptionsPattern[] is very permissive:

foo[opts:OptionsPattern[]]:=opts

foo[2]
foo[{}]
foo[{{}}]
foo[a -> 1, b -> 2]
foo[{a -> 1, b -> 2}]
foo[{{a -> 1, b -> 2}}]
foo[{{{a -> 1, b -> 2}}}]

prints:

foo[2]

{}

{{}}

Sequence[a -> 1, b -> 2]

{a -> 1, b -> 2}

{{a -> 1, b -> 2}}

{{{a -> 1, b -> 2}}}

This makes options processing tedious, for instance checking if an option with key k is present is not as simple as it should be. The code below

foo[k_Symbol,opts:OptionsPattern[]]:=MemberQ[Keys[opts],k]

does not work as expected:

foo[a,{}]              (* OK *)
foo[a,{a->1,b->2}]     (* OK *)
foo[a,{{a->1,b->2}}]   (* Problem *)
foo[a,a->1,b->2]       (* Problem *)

outputs:

False

True

False <- Should be True

False <- Should be True

How to fix these problems?

$\endgroup$
4
$\begingroup$

Updated:

To manage this I finally have introduced a small MMA package SafeOptions

(I initially wanted to put all the details here but it was longer than expected, sorry for that).

To use the package

<<SafeOptions`

you must modify foo3 as follows:

Options[foo3] = createOptionList[{a->10, b->1}, Options /@ {foo1, foo2}];

foo3[opts : OptionsPattern[]] :=      
    Block[{safeOpts},

          If[(safeOpts = getOptionList[Options[foo3], opts]) === $Failed, 
		 Return[$Failed]];

          foo1[1, 2, filterOptionList[Options[foo1], safeOpts]]; 
          foo2[4, 5, filterOptionList[Options[foo2], safeOpts]];
    ];

Now

foo3[]

prints:

foo1 a=10 b=1

foo2 b=1

as expected. The options are correctly forwarded.

Also observe that if we do not provide the default option value b->1

Options[foo3] = createOptionList[{a->10}, Options /@ {foo1, foo2}]

an error message is generated in case of incompatible option values:

safeOptions::incompatibleOptions: Some options are incompatibles {b->{2,-2}}

$Failed


Concerning OptionsPattern[]: the package provides the normalizeOptionPattern[] function that transforms all these different forms into a well defined list of transformation rules. Now,

foo[opts:OptionsPattern[]]:=normalizeOptionPattern[opts]

foo[2]
foo[{}]
foo[{{}}]
foo[a -> 1, b -> 2]
foo[{a -> 1, b -> 2}]
foo[{{a -> 1, b -> 2}}]
foo[{{{a -> 1, b -> 2}}}]

prints:

foo[2]

{}

{}

{a -> 1, b -> 2}

{a -> 1, b -> 2}

{a -> 1, b -> 2}

{a -> 1, b -> 2}

and

foo[k_Symbol,opts:OptionsPattern[]]:=MemberQ[Keys[normalizeOptionPattern[opts]],k]

works as expected:

foo[a,{}]              (* OK *)
foo[a,{a->1,b->2}]     (* OK *)
foo[a,{{a->1,b->2}}]   (* OK *)
foo[a,a->1,b->2]       (* OK *)

outputs:

False

True

True

True


For further details you can visit the GitHub package page where there is a first version of the SafeOptions package. Feel free to test it/report bugs, etc.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.