11

The background

This example works as expected, small function with a special case rule:

foo // ClearAll    
foo // Options = {"bar" -> 1};    

foo[x_, y : Except[_Rule], patt : OptionsPattern[]
] := {x, y, OptionValue["bar"]};

foo[x_, opts : OptionsPattern[]] := foo[x, "def", opts]

foo["xx", "bar" -> 3]
{"xx", "def", 3}

Except[_Rule] is needed because I don't want f["xx", "bar"->1] to go directly to the first definition and treat the option as y.

The problem

However, originally I used:

...
foo[x_, y : Except[OptionsPattern[]], patt : OptionsPattern[] ]:=...
...

but then

foo // ClearAll
foo // Options = {"bar" -> 1};

foo[x_, y : Except[OptionsPattern[]], patt : OptionsPattern[]
] := {x,  y, OptionValue["bar"]};

foo[x_, opts : OptionsPattern[]] := foo[x, "def", opts]

foo["xx", "bar" -> 3]
{"xx", "def", 1}

The question

Why wasn't the "bar" passed down?

I don't have time to investigate and will proceed with the workaround but would like to understand that as nothing obvious comes to my mind now.

Mr.Wizard
  • 271,378
  • 34
  • 587
  • 1,371
Kuba
  • 136,707
  • 13
  • 279
  • 740
  • What do you expect from something like foo["xx", MaxRecursion -> 3], with non-foo option? (I expected an error but didn't get one.) The problem seems to be that patt is not passed to OptionValue, so another workaround is to use the full form OptionValue[foo, patt, "bar"]. Perhaps it's confused by the two occurrences of OptionsPattern[]? – Michael E2 Aug 03 '17 at 12:41
  • 3
  • @MichaelE2 yes, error would be expected. – Kuba Aug 03 '17 at 12:44
  • 1
    I've hit this block before, I know, when doing meta-programming. My go-to now is to use a construct like Except[_?OptionQ] which seems to be reasonably solid and less-restrictive than Except[_Rule] – b3m2a1 Aug 03 '17 at 14:49
  • @b3m2a1 That's exactly the solution I first proposed (see link above) but I think a case can be made for Shortest being superior, which I learned about later. – Mr.Wizard Aug 03 '17 at 16:22

1 Answers1

10

Recall that OptionValue is a "magic" symbol with nonstandard behavior. The expression OptionValue["bar"] "by magic" gets its value without it being explicitly passed one, working with its "magic" brother OptionsPattern in a not-entirely-transparent way. I think it attaches to the first OptionsPattern[] object which is not used in the final matching with "bar" -> 3.

I don't yet have a clear idea of what is happening but I can give other examples of weird behavior.

exhibit A

ClearAll[foo]
Options[foo] = {"bar" -> 1};

foo[
  first : Except[OptionsPattern[]],
  second : OptionsPattern[]
] := {{first}, {second}}

foo["arg", "bar" -> 3]
{{"bar" -> 3}, {}}
  • "arg" disappears entirely. Theoretically for the rule on foo to apply "arg" should be attached to either first or second patterns, but instead it vanishes as though attached to a third pattern expression

  • "bar" -> 3 is attached to first, implying that it matches Except[OptionsPattern[]], despite the fact that MatchQ["bar" -> 3, Except[OptionsPattern[foo]]] returns False

exhibit B

Let's see what happens if we name the first appearance of OptionsPattern[] itself.

ClearAll[foo]
Options[foo] = {"bar" -> 1};

foo[
  first : Except[op1 : OptionsPattern[]], 
  second : OptionsPattern[]
] := {{first}, {second}, {op1}}

foo["arg", "bar" -> 3]
{{}, {}, {"bar" -> 3}}
  • Once again "arg" goes missing, but this time "bar" -> 3 is not attached to first as it was before, leaving both first and second empty. Instead it is attached (only) to op1.

a note about Except

Except at times causes strange behavior. For example, I believe from an earlier question I cannot at the moment find:

Cases[{1, 0, 2, 0, 3}, Except[x : 0] :> x]
{Removed["$Variable"][1], Removed["$Variable"][1], Removed["$Variable"][1]}

Combining this somewhat peculiar pattern construct with the "magic" of OptionsPattern may simply be a bad idea at this time, unless and until this edge case is specifically addressed by the developers.


version differences

In version 10.1 from your second block of code I get:

In[6]:= foo["xx", "bar" -> 3]

During evaluation of In[6]:= OptionValue::rep: def is not a valid replacement rule. >>

Out[6]= {"xx", "bar" -> 3, 1}

Did I enter it wrong or has behavior changed?

Mr.Wizard
  • 271,378
  • 34
  • 587
  • 1,371
  • 1
    It changed, already v10.4 returns {"xx", "def", 1}. – Kuba Aug 03 '17 at 13:03
  • @Kuba Thank you for highlighting this weird behavior. I'll be looking at this more. Need coffee. – Mr.Wizard Aug 03 '17 at 13:05
  • @Kuba Do any of the examples in my post behave differently in 10.4? – Mr.Wizard Aug 03 '17 at 15:28
  • 1
    Yes, both, and 10.4 output is the same as 11.1.1's. That is: A) {{"arg"}, {"bar" -> 3}} and B) {{"arg"}, {"bar" -> 3}, {}} – Kuba Aug 04 '17 at 08:14
  • @Kuba Well that's good, I think. Do you get the desired output if you use foo[x_, y : Except[OptionsPattern[]], patt : OptionsPattern[] ] := {x, y, OptionValue[foo, patt, "bar"]}; ? – Mr.Wizard Aug 04 '17 at 11:58