20
$\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$
    – Mr.Wizard
    May 5, 2015 at 8:02
  • 2
    $\begingroup$ FWIW I believe you could also use mergeRules = DeleteDuplicatesBy[First] (just a small comment.) $\endgroup$
    – C. E.
    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$
    – Kuba
    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$
    – Mr.Wizard
    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$
    – Carl Woll
    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$
    – Kuba
    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$
    – Mr.Wizard
    May 5, 2015 at 8:18
  • 1
    $\begingroup$ @J. M. Can you give me an example of the "not" case? $\endgroup$
    – Mr.Wizard
    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$
    – jkuczm
    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$
    – jkuczm
    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$
    – Mr.Wizard
    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$
    – jkuczm
    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$
    – bdforbes
    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$
    – jkuczm
    Jun 24, 2015 at 13:50

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.