You can try the following:
Attributes[HandleLegacyOption] = {HoldAll};
HandleLegacyOption[o : OptionValue[sym_, opts_, name_]] :=
o //. Fallback[old_] :> OptionValue[sym, opts, old]
This can be used the following way:
Options[f] = {"bar" -> Fallback["foo"], "foo" -> 1};
f[OptionsPattern[]] := HandleLegacyOption@OptionValue["bar"]
f[]
f["bar" -> 2]
f["foo" -> 2]
f["foo" -> 2, "bar" -> 3]
(* 1 *)
(* 2 *)
(* 2 *)
(* 3 *)
As you can see, setting either option works, and the new name takes precedence over the old one.
How?
Since OptionValue
is a very special function, we can't do much other than explicitly leaving OptionValue[…]
in the r.h.s. of our definitions. But one thing we can use is the fact that OptionValue[…]
constructs are always expanded, no matter where they appear (see also linked question):
g[OptionsPattern[]] := Hold@OptionValue["foo"]
g["bar" -> 1]
(* Hold[OptionValue[g, {"bar" -> 1}, "foo"]] *)
So as long as we have OptionValue[…]
explicitly appearing, we have access to:
- The symbol, and thus the defaults
- The explicitly specified options
- The queried options
The function HandleLegacyOption
above uses this information by repeatedly querying option values as long as the result is Fallback[oldName]
. This essentially defaults the new option to the value of another option.
Possible extensions
As mentioned earlier, we need OptionValue
to appear on the r.h.s. of the definition, otherwise we won't get the automatic expansion of all the information we need. One possible way to (partially) automate this wrapping of OptionValue
might be:
HoldPattern[lhs_ // AddLegacyOptionHandling := rhs_] ^:=
Hold[rhs] /.
o_OptionValue :> HandleLegacyOption@o /.
Hold[proc_] :> (lhs := proc)
This automatically wraps all OptionValue
expressions on the r.h.s. in HandleLegacyOption
, e.g.
f[OptionsPattern[]] // AddLegacyOptionHandling := OptionValue["bar"]
yields the same result as in the first example.
Alternative solution
Note: This is heavily based on @Henrik Schumacher's answer, so be sure to upvote that one if this is useful
Using the idea of adding special casing for certain symbols to OptionValue
, we get the following solution:
processing = False;
AddLegacyOptionHandling[sym_] := (
OptionValue[sym, opts_, names_] /; ! processing ^:= Block[
{processing = True},
OptionValue[sym, opts, names] //. Fallback[old_] :> OptionValue[sym, opts, old]
]
)
After calling AddLegacyOptionHandling[f]
, this works exactly as in the examples above.
The following version also supports the fourth argument of OptionValue
:
processing = False;
Attributes[OptionWrapper] = {Flat, HoldAll};
AddLegacyOptionHandling[sym_] := (
OptionValue[sym, opts_, names_, wrapper_ | PatternSequence[]] /; ! processing ^:= Block[
{processing = True},
OptionValue[sym, opts, names, OptionWrapper] //.
Fallback[old_] :> With[
{val = OptionValue[sym, opts, old, OptionWrapper]},
val /; True
] /.
Verbatim[OptionWrapper][val_] :> If[{wrapper} === {}, val, wrapper[val]]
]
)
The code is slightly more complex now as we need to be careful with evaluation leaks. But all in all, this version should support all forms of OptionValue
, that is both lists of option names and Hold
wrappers, while incurring a negligible performance hit for options that are not set to Fallback[…]
and no impact for unaffected functions.