21

the case

I want to be able to create a function with some default options but also without need to add full explicit list of options available for it.

And then inside I want to be able to filter from given and default options, those which are Button options or Tooltip options for example.

So something like:

Options[f] = {(*list of default options*)}

f[args__, OptionsPattern[]]:=Column[{
   (*Options that are suitable for Button*),
   (*Options that are suitable for Tooltip*),
   OptionValue[(*specific name*)]
 }]

And I wasn't able to get this with built in Options management functions: OptionsPattern[], OptionValue, FilterRules etc.

additional requirements

  1. I want to avoid Options[f] = Join[customOptions, Options[Button], ...].

    I don't think is a good solution, there may be duplicates in customOptions for them and an explicit list of Options[f] grows.

  2. I want to be able to provide any option to the function without error messsage e.g.: Unknown option Apparance for f...

  3. We can get 2. by skipping OptionsPattern[] in definition but without it we can't use built in OptionValue. I want to be able to refer to functions by their names.

  4. Rules filtering mechanism should not produce duplicates. I know Button[..., ImageSize->300, ImageSize->200] will behave stable but I find it ugly.

my approach

(* auxiliary functions *)

mergeRules = GatherBy[Join[##], First][[All, 1]] &;

optionValue = #2 /. # &;

(* function definition *)

ClearAll[f];
Options[f] = {"Test" -> 1, ImageSize -> 100, TooltipDelay -> 20};

f[x_, optionsPattern : (_Rule | _RuleDelayed) ...] := With[{
   opt = mergeRules[{optionsPattern}, Options[f]]}
  ,
  Column@{
    FilterRules[opt, Options@Button],
    FilterRules[opt, Options@Tooltip],
    optionValue[opt, "Test"]
    }
  ]

So I need to start my definitions with With[{ opt = mergeRules[ {optionsPattern}, Options[f]]}, which does not seem to be a big problem, but why I have to do this?

tests

f[1, Appearance -> "Palette"]
{Appearance->Palette, ImageSize->100} 
{TooltipDelay->20}
1
f[1, ImageSize -> 200]
{ImageSize->200}
{TooltipDelay->20}
1
f[1]
{ImageSize->100}
{TooltipDelay->20}
1

question

Is there simpler approach, with built functions maybe? Or should I include Options[Button] etc. to Options[f] and count on the fact that when given duplicates, first one wins?

Edits

Mr.Wizard's answer fulfills points:

1 automatically, 2/3 by using OptionsPattern[{f,Button, ...}]. So still 4 needs custom filtering function but it is a good answer anyway.

Kuba
  • 136,707
  • 13
  • 279
  • 740

2 Answers2

12

Following your clarification this seems to be OK, though I would agree that a cleaner solution would be nice:

Options[f] = {foo -> bar, ImageSize -> 333};

f[args__, opts : OptionsPattern[{f, Button, Tooltip}]] :=
 Append[
   FilterRules[{opts, Options @ f}, Options @ #] & /@ {Button, Tooltip},
   OptionValue[foo]
 ] // Column

Test:

f[1, 2, Background -> Blue, AutoAction -> False, TooltipDelay -> 1]
{Background -> RGBColor[0, 0, 1], AutoAction   -> False, ImageSize -> 333}
{Background -> RGBColor[0, 0, 1], TooltipDelay -> 1}
bar

Following your updated requirements the only streamlining I can think to recommend is to combine the functionality of your mergeRules with that of FilterRules. This is a trivial refactoring but again I hope you find some value in the idea.

getRules[base_Symbol, op___][target_Symbol] :=
  First /@ GatherBy[{op, Options @ base} ~FilterRules~ Options[target], First]

Options[f] = {foo -> bar, ImageSize -> 333};

f[args__, opts : OptionsPattern[{f, Button, Tooltip}]] := 
 Append[getRules[f, opts] /@ {Button, Tooltip}, OptionValue[foo]] // Column

test:

f[1, 2, Background -> Blue, AutoAction -> False, TooltipDelay -> 1, ImageSize -> 99]
{Background -> RGBColor[0, 0, 1], AutoAction   -> False, ImageSize -> 99}
{Background -> RGBColor[0, 0, 1], TooltipDelay -> 1}
bar

getRules could also be written with KeyTake

getRules[base_Symbol, op___][target_Symbol] :=
  {op, Options @ base} // Flatten // KeyTake[Keys @ Options @ target] // Normal
Mr.Wizard
  • 271,378
  • 34
  • 587
  • 1,371
  • Should I live with Button[...., ImageSize->200, ImageSize->333]?, it is ugly but works, could it be a problem anywhere? – Kuba May 05 '15 at 08:15
  • @Kuba I don't recall ever seeing that syntax produce an error, and in fact I have found internal implementations that result in duplicate rules so it seems to be accepted practice at WRI; however I added a GatherBy filter for this case in my own code here: (46925) – Mr.Wizard May 05 '15 at 08:18
  • More often than not, when two or more option settings are given to a function, the leftmost one is the one applied, and the rest are ignored. – J. M.'s missing motivation May 06 '15 at 09:21
  • 1
    @J. M. Can you give me an example of the "not" case? – Mr.Wizard May 06 '15 at 09:22
  • Wizard, sadly my memory fails me here. :( But at least for all the usual system functions, the "often" is in fact "always". – J. M.'s missing motivation May 06 '15 at 09:25
  • 3
    @J. M., @Mr.Wizard Options[f] = {a -> x}; SetOptions[f, a -> y, a -> z] uses the rightmost option. – jkuczm May 06 '15 at 16:35
  • @jkuczm I wouldn't say those are options for SetOptions, rather arguments. But good point. – Kuba May 06 '15 at 23:11
  • 1
    @Kuba I agree, but I was bitten by it one time. I was aggregating options like this newOpts = {opt -> val, oldOpts}. I thought f[newOpts] would work the same as SetOptions[f, newOpts];f[] (except obvious fact that the latter sets options permanently), but it doesn't. – jkuczm May 07 '15 at 11:08
9

Let's start with slightly modified version of mergeRules, that takes into account fact that options can have symbolic or string names and name -> val is treated the same as "name" -> val:

ClearAll[symbolToName, deleteOptionDuplicates]

symbolToName[sym_Symbol] := SymbolName[sym]
symbolToName[arg_] := arg

deleteOptionDuplicates[opts:OptionsPattern[]] :=
    GatherBy[Flatten[{opts}], Composition[symbolToName, First]][[All, 1]]

Now we can define an environment, providing special option-filtering function:

ClearAll[withOptions, getOptions]
withOptions[base_Symbol, opts___] :=
    Function[body,
        With[{allOptions = deleteOptionDuplicates[opts, Options[base]]},
            Block[{getOptions = FilterRules[allOptions, Options[#]] &},
                body
            ]
        ],
        HoldFirst
    ]

To make our environment easier to use, in function definitions, let's add some macro tricks stolen from Leonid:

withOptions /: 
    Verbatim[SetDelayed][lhs_, rhs : HoldPattern[withOptions[__][_]]] :=
        Block[{With},
            Attributes[With] = {HoldAll};
            lhs := Evaluate[rhs]
        ]
withOptions /: 
    Verbatim[SetDelayed][
        h_[pre___, optsPatt_OptionsPattern, post___],
        HoldPattern[withOptions[body_]]
    ] :=
        h[pre, opts : optsPatt, post] := withOptions[h, opts][body]
withOptions /: 
    Verbatim[SetDelayed][
        h_[
            pre___,
            namedOptsPatt:Verbatim[Pattern][optsName_, _OptionsPattern],
            post___
        ], 
        HoldPattern[withOptions[body_]]
    ] :=
        h[pre, namedOptsPatt, post] := withOptions[h, optsName][body]

I guess that extraction of OptionsPattern from lhs could be more general.

Now we can define functions like this:

ClearAll[f]
Options[f] = {"Test" -> 1, ImageSize -> 100, TooltipDelay -> 20};
f[x_, OptionsPattern[{f, Button, Tooltip}]] :=
    withOptions@Column@
        {getOptions[Button], getOptions[Tooltip], OptionValue["Test"]}

It gives expected results:

f[1, Appearance -> "Palette"]
{Appearance->Palette, ImageSize->100} 
{TooltipDelay->20}
1
f[1, ImageSize -> 200]
{ImageSize->200}
{TooltipDelay->20}
1
f[1]
{ImageSize->100}
{TooltipDelay->20}
1

If there are duplicates, first option is used regardless of whether it's name is a symbol or a string:

f[1, "ImageSize" -> 1, ImageSize -> 2, ImageSize -> 3]
{ImageSize->1}
{TooltipDelay->20}
1

Usually I don't need to store or return filtered options, I just want to pass them to appropriate function. In such cases other environment, that setts default option values for some functions, can be useful:

ClearAll[withDefaultOptions]
withDefaultOptions[base_Symbol, targets:{__Symbol}, opts___]:=
    Function[body,
        With[{allOptions=deleteOptionDuplicates[opts, Options[base]]},
            Internal`InheritedBlock[targets,
                Scan[
                    SetOptions[#, FilterRules[allOptions, Options[#]]]&,
                    targets
                ];
                body
            ]
        ],
        HoldFirst
    ]
withDefaultOptions /:
    Verbatim[SetDelayed][
        lhs_,
        rhs:HoldPattern[withDefaultOptions[_, _, ___][_]]
    ] :=
        Block[{With},
            Attributes[With]={HoldAll};
            lhs := Evaluate[rhs]
        ]
withDefaultOptions /:
    Verbatim[SetDelayed][
        h_[pre___, optsPatt_OptionsPattern, post___],
        HoldPattern[withDefaultOptions[body_]]
    ] :=
        h[pre, opts:optsPatt,post] :=
            withDefaultOptions[
                h,
                Cases[Flatten[{First[optsPatt]}], Except[h, _Symbol]],
                opts
            ][body]
withDefaultOptions /:
    Verbatim[SetDelayed][
        h_[
            pre___,
            namedOptsPatt:Verbatim[Pattern][optsName_, optsPatt_OptionsPattern],
            post___
        ],
        HoldPattern[withDefaultOptions[body_]]
    ] :=
        h[pre, namedOptsPatt, post] :=
            withDefaultOptions[
                h,
                Cases[Flatten[{First[optsPatt]}], Except[h,_Symbol]],
                optsName
            ][body]

Let's start with a dummy function, to which we'll pass options:

ClearAll[f];
Options[f] = {"optA" -> "valFA", "optB" -> "valFB"};
f[OptionsPattern[]] := OptionValue[{"optA", "optB"}]

Now a function that can accept and pass options to f. It has its own options and some overridden f options, with different defaults.

ClearAll[g];
Options[g] = {"optA" -> "valGA", "optC" -> "valGC"};
g[OptionsPattern[{g, f}]] :=
    withDefaultOptions@{OptionValue[{"optA", "optC"}], f[]}

Notice that we don't have to pass anything to f, in body of g. Proper default options for f are set automatically, by withDefaultOptions, based on what is matched by OptionsPattern and what is inside OptionsPattern.

If option for f is neither given explicitly, nor set as default on g, then default of f is used.

g[]
(* {{"valGA", "valGC"}, {"valGA", "valFB"}} *)

g["optA" -> 1]
(* {{1, "valGC"}, {1, "valFB"}} *)

g["optB" -> 1]
(* {{"valGA", "valGC"}, {"valGA", 1}} *)

g["optC" -> 1]
(* {{"valGA", 1}, {"valGA", "valFB"}} *)
jkuczm
  • 15,078
  • 2
  • 53
  • 84
  • I'll be honest: withOptions is more fluff than I care for. However withDefaultOptions is a bloody brilliant idea! – Mr.Wizard May 07 '15 at 13:26
  • @Mr.Wizard I agree concerning withOptions. Actually I had withDefaultOptions ready. withOptions was created just to show this macro/environment idea while fulfilling requirements from question. – jkuczm May 07 '15 at 13:44
  • 2
    Why do you wrap Verbatim around SetDelayed? I wouldn't have expected SetDelayed to evaluate prematurely here. – bdforbes Jun 23 '15 at 02:43
  • @bdforbes Honestly, I mindlessly copied it from LetL macro. Since it's also used in built-in Macros`DeclareMacro, I thought there must be some subtlety that I'm missing, but you are right that everything seems to work the same without this Verbatim. – jkuczm Jun 24 '15 at 13:50