21
$\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$
5
  • $\begingroup$ Related: (353), (20470) $\endgroup$ Commented May 5, 2015 at 8:02
  • 2
    $\begingroup$ FWIW I believe you could also use mergeRules = DeleteDuplicatesBy[First] (just a small comment.) $\endgroup$ Commented May 6, 2015 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$ Commented May 6, 2015 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$ Commented May 6, 2015 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$ Commented Mar 23, 2017 at 17:34

2 Answers 2

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$
8
  • $\begingroup$ Should I live with Button[...., ImageSize->200, ImageSize->333]?, it is ugly but works, could it be a problem anywhere? $\endgroup$ Commented May 5, 2015 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$ Commented May 5, 2015 at 8:18
  • 1
    $\begingroup$ @J. M. Can you give me an example of the "not" case? $\endgroup$ Commented May 6, 2015 at 9:22
  • 3
    $\begingroup$ @J. M., @Mr.Wizard Options[f] = {a -> x}; SetOptions[f, a -> y, a -> z] uses the rightmost option. $\endgroup$ Commented May 6, 2015 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$ Commented May 7, 2015 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$
4
  • $\begingroup$ I'll be honest: withOptions is more fluff than I care for. However withDefaultOptions is a bloody brilliant idea! $\endgroup$ Commented May 7, 2015 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$ Commented May 7, 2015 at 13:44
  • 2
    $\begingroup$ Why do you wrap Verbatim around SetDelayed? I wouldn't have expected SetDelayed to evaluate prematurely here. $\endgroup$ Commented Jun 23, 2015 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$ Commented Jun 24, 2015 at 13:50

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.