18
$\begingroup$

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.

$\endgroup$
  • $\begingroup$ Related: (353), (20470) $\endgroup$ – Mr.Wizard May 5 '15 at 8:02
  • 2
    $\begingroup$ FWIW I believe you could also use mergeRules = DeleteDuplicatesBy[First] (just a small comment.) $\endgroup$ – C. E. May 6 '15 at 9:44
  • $\begingroup$ @Pickett Thanks for pointing this out. However, I would like to avoid functions introduced in V10 so it can be useful for more people. $\endgroup$ – Kuba May 6 '15 at 9:46
  • $\begingroup$ @Pickett I forgot about that function. I guess I put it out of mind when I didn't like the performance. :-/ $\endgroup$ – Mr.Wizard May 6 '15 at 11:27
  • $\begingroup$ I prefer using Options[f] = fullList. I find it annoying when I use Options[f] and get back an incomplete or empty list. $\endgroup$ – Carl Woll Mar 23 '17 at 17:34
12
$\begingroup$

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
$\endgroup$
  • $\begingroup$ Should I live with Button[...., ImageSize->200, ImageSize->333]?, it is ugly but works, could it be a problem anywhere? $\endgroup$ – Kuba May 5 '15 at 8:15
  • $\begingroup$ @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) $\endgroup$ – Mr.Wizard May 5 '15 at 8:18
  • 1
    $\begingroup$ @J. M. Can you give me an example of the "not" case? $\endgroup$ – Mr.Wizard May 6 '15 at 9:22
  • 3
    $\begingroup$ @J. M., @Mr.Wizard Options[f] = {a -> x}; SetOptions[f, a -> y, a -> z] uses the rightmost option. $\endgroup$ – jkuczm May 6 '15 at 16:35
  • 1
    $\begingroup$ @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. $\endgroup$ – jkuczm May 7 '15 at 11:08
9
$\begingroup$

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"}} *)
$\endgroup$
  • $\begingroup$ I'll be honest: withOptions is more fluff than I care for. However withDefaultOptions is a bloody brilliant idea! $\endgroup$ – Mr.Wizard May 7 '15 at 13:26
  • $\begingroup$ @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. $\endgroup$ – jkuczm May 7 '15 at 13:44
  • 2
    $\begingroup$ Why do you wrap Verbatim around SetDelayed? I wouldn't have expected SetDelayed to evaluate prematurely here. $\endgroup$ – bdforbes Jun 23 '15 at 2:43
  • $\begingroup$ @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. $\endgroup$ – jkuczm Jun 24 '15 at 13:50

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.