5
$\begingroup$

I am building lots of functions that use Options and OptionValue and wanted to reduce the redundancy in the expressions that define the functions. I will show a tiny example, here, but please imagine that instead of a couple of options with names like a and b, my real examples have dozens of options with names like

blancmangeWithRaisinsAndPartiallyHydrogenatedChocolateSauce

and you will easily understand why I want to do what I want to do.

What do I want to do? My functions fit the following pattern:

The following defines the acceptable optional parameters for foo: they are a and b, and they have default values 1 and 2, respectively. The acceptable options are defined separately from the body of the function. A caller may supply values for a or b or both or neither, using option syntax below.

ClearAll[foo];
Options[foo] = {"a" -> 1, "b" -> 2};

The following defines the body of the function. This function produces an association object with attributes corresponding to the values of the optional parameters.

foo[OptionsPattern[]] :=
  <|"a" -> OptionValue["a"],
   "b" -> OptionValue["b"]|>;

The following is a call that supplies only the optional value for a.

foo["a" -> 42]
<|"a" -> 42, "b" -> 2|>

The following is a call that supplies both a and b; notice that the order does not matter.

foo["b" -> 43, "a" -> 42]
<|"a" -> 42, "b" -> 43|>

The following is a call that supplies neither a nor b.

foo[]
<|"a" -> 1, "b" -> 2|>

I want to reduce the redundancy in the definition expression for foo. I define a helper:

ClearAll[opt];
opt[nym_] := Rule[nym, OptionValue[nym]];

Hoping for a new style of definition for foo:

foo[OptionsPattern[]] = <|opt["a"], opt["b"]|>

Notice that I used Set and not SetDelayed because I wanted the right-hand side to be evaluated at definition time, not at call time. But still no dice: foo doesn't work any more

foo[]
<|"a" -> OptionValue["a"], "b" -> OptionValue["b"]|>

Its options are still in-force

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

Why doesn't my little trick work? Why doesn't opt rewrite in the context of the definiing expression for foo?

$\endgroup$

3 Answers 3

6
$\begingroup$

I think your problem is probably related to the way OptionValue[name] is evaluated. When you use a regular List instead of an Association, you'll find the whole thing works. I don't have the mathematica-fu to understand why in detail, but here is an alternative solution that makes your function definitions pretty concise.

Define a utility function that reads options for a symbol, converts to an association, and merges in another set of options as an association:

getem[sym_, opts_: {}] := Association[Association[Options[sym]], Association[opts]];

You first of all set your options for your symbol:

Options[bob] = { "c" -> 3, "d" -> 4 };

Now your function definitions are pretty terse:

bob[opts : OptionsPattern[]] := getem[bob, opts];

And it evaluates the way you want:

bob[]

<|"c" -> 3, "d" -> 4|>

bob["d" -> 59]

<|"c" -> 3, "d" -> 59|>

As another exercise you could probably meta-program your way into generating the bob[] style functions, but I have not plumbed the depths of mathematica there as yet :) I'm confident it's possible based on some of the crazy stuff I've seen so far.

$\endgroup$
2
  • $\begingroup$ Clever merging of Associations and works well. $\endgroup$
    – Reb.Cabin
    Aug 25, 2014 at 12:56
  • $\begingroup$ This is a better answer because I only need to mention each option string once: minimal opportunity for typos. $\endgroup$
    – Reb.Cabin
    Aug 25, 2014 at 21:45
5
$\begingroup$

I believe this results from Association being atomic (or "not NormalQ" as Taliesin puts it) without being fully overloaded to behave as a normal expression of equivalent structure. Observe:

asc = <|"a" -> q, "b" -> r, "c" -> s|>;
Block[{q = 1, r = 2, s = 3}, asc]
<|"a" -> q, "b" -> r, "c" -> s|>

Also:

With[{q = 1, r = 2, s = 3}, Evaluate @ asc]
<|"a" -> q, "b" -> r, "c" -> s|>

One might expect that because the association is constructed from Rule rather than RuleDelayed the Values (right-hand-sides) would evaluate inside the Block but they do not. Further, one might think that the seemingly literal appearance of q, r, and s in the Association would allow them to be replaced using With but again it does not. In these ways Association objects diverge from the normal evaluation rules.

Your original definition works because the right-hand-side is not an actual Association object: it is merely input syntax for one. Therefore the OptionValue["a"] etc. are both substituted and evaluated before the Association is constructed.

To make your pattern work we merely need to inject the body of the association while allowing the <| |> syntax to remain held via SetDelayed:

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

opt[nym_] := Rule[nym, OptionValue[nym]];

(foo[OptionsPattern[]] := <|##|>) &[opt["a"], opt["b"]]

Test:

foo["a" -> 7]
<|"a" -> 7, "b" -> 2|>
$\endgroup$
4
  • $\begingroup$ With[{opts = Sequence[opt["a"], opt["b"]]}, foo[OptionsPattern[]] := <|opts|>] also seems to work. $\endgroup$
    – seismatica
    Aug 25, 2014 at 7:25
  • $\begingroup$ @seismatica I guess it would, but ## & is shorter. :-) $\endgroup$
    – Mr.Wizard
    Aug 25, 2014 at 7:29
  • $\begingroup$ An important (if very oblique) clue is in the documentation for Association: "Normal converts an Association to a list of rules." That implies that Association isn't normal, lest Normal do anything at all to it. $\endgroup$
    – Reb.Cabin
    Aug 25, 2014 at 12:30
  • $\begingroup$ @Reb.Cabin Unfortunately that doesn't really tell us much since Normal works on a number of standard expressions as well. (e.g. GraphicsComplex.) Further, even if Association is atomic it could still behave more like a standard expression; SparseArray is also atomic yet you will find that the examples using Block and With do affect its contents. $\endgroup$
    – Mr.Wizard
    Aug 25, 2014 at 19:03
0
$\begingroup$

Posting this as an answer because it's too long for a comment and it's a significant correction and amplification of @Hagus's answer.

First, we need for options to be a BlankNullSequence to handle natural call forms where options need not be packaged in lists:

ClearAll[constructorWithOptions];
constructorWithOptions[symbol_, options___Rule] :=
  <|<|Options[symbol]|>, <|options|>|>

ClearAll[foo];
Options[foo] = {"a" -> 1, "b" -> 2};
foo[options : OptionsPattern[]] := constructorWithOptions[foo, options];
foo[]
foo["a" -> 41]
foo["b" -> 42]
foo["b" -> 42, "a" -> 41]
foo["c" -> 43]
foo["a" -> 41, "c" -> 43]
foo["b" -> 42, "c" -> 43]
foo["b" -> 42, "c" -> 43, "a" -> 41]
<|"a" -> 1, "b" -> 2|>
<|"a" -> 41, "b" -> 2|>
<|"a" -> 1, "b" -> 42|>
<|"a" -> 41, "b" -> 42|>
<|"a" -> 1, "b" -> 2, "c" -> 43|>
<|"a" -> 41, "b" -> 2, "c" -> 43|>
<|"a" -> 1, "b" -> 42, "c" -> 43|>
<|"a" -> 41, "b" -> 42, "c" -> 43|>

This construct admits options that are not defined for foo. This may be acceptable if we're allowing mixins, but if we want the more familiar behavior of rejecting unknown options, we can do something like this:

ClearAll[constructorWithOnlyOptions];
constructorWithOnlyOptions[symbol_, options___Rule] := (
  symbol::nodef = "unknown option `1` for " <> ToString[symbol];
  With[{
      acceptableNames = First /@ Options[symbol],
      proposedNames = First /@ {options}},
    With[{
        acceptedProposals = Intersection[acceptableNames, proposedNames],
        rejectedProposals = Complement[proposedNames, acceptableNames]},
      Message[symbol::nodef, #] & /@ rejectedProposals;
      <|<|Options[symbol]|>, <|
        Select[{options}, MemberQ[acceptedProposals, First[#]]& ]|>|> ] ]);

ClearAll[foo];
Options[foo] = {"a" -> 1, "b" -> 2};
foo[options : OptionsPattern[]] := constructorWithOnlyOptions[foo, options];
foo[]
foo["a" -> 41]
foo["b" -> 42]
foo["b" -> 42, "a" -> 41]
foo["c" -> 43]
foo["a" -> 41, "c" -> 43]
foo["b" -> 42, "c" -> 43]
foo["b" -> 42, "c" -> 43, "a" -> 41]
<|"a" -> 1, "b" -> 2|>
<|"a" -> 41, "b" -> 2|>
<|"a" -> 1, "b" -> 42|>
<|"a" -> 41, "b" -> 42|>
foo::nodef: unknown option c for foo
<|"a" -> 1, "b" -> 2|>
foo::nodef: unknown option c for foo
<|"a" -> 41, "b" -> 2|>
foo::nodef: unknown option c for foo
<|"a" -> 1, "b" -> 42|>
foo::nodef: unknown option c for foo
<|"a" -> 41, "b" -> 42|>

This isn't perfect, because it associates the Message with foo instead of with OptionValue, but I think it's good enough for production use.

Now, encapsulate the options:

ClearAll[defineConstructor];
defineConstructor[symbol_] :=
  ( symbol[options:OptionsPattern[]] :=
    constructorWithOnlyOptions[symbol, options] );

ClearAll[foo];
Options[foo] = {"c" -> 3, "d" -> 4};
defineConstructor[foo];
foo[]
foo["c" -> 43]
foo["d" -> 44]
foo["d" -> 44, "c" -> 43]
foo["e" -> 45]
foo["e" -> 46, "d" -> 44, "f" -> 46]
<|"c" -> 3, "d" -> 4|>
<|"c" -> 43, "d" -> 4|>
<|"c" -> 3, "d" -> 44|>
<|"c" -> 43, "d" -> 44|>
foo::nodef: unknown option e for foo
<|"c" -> 3, "d" -> 4|>
foo::nodef: unknown option e for foo
foo::nodef: unknown option f for foo
<|"c" -> 3, "d" -> 44|>
Options[foo]
{"c" -> 3, "d" -> 4}
$\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.