Renaming options of custom functions while preserving backwards compatibility

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.


This is my new approach. It is minimally invasive in the sense that it has to redefine OptionValue to handle only the new option "newopt" for the function f differently.

Now declare a function f in the classical way, but set the defaults for all "old" options to Automatic (or to any other custom symbol):

Options[f] = {
   "newopt" -> 1,
   "oldopt" -> Automatic
   };
f[opts : OptionsPattern[]] := OptionValue["newopt"]

Add a new rule to OptionValue that is associated to f:

optionAliases = <|"newopt" -> {"oldopt"}|>;

f /: OptionValue[f, opts_, "newopt"] := If[
   ("newopt" /. {opts}) =!= {"newopt"},
   First[OptionValue[f, opts, {"newopt"}]],
   First[OptionValue[f, opts, optionAliases["newopt"]]] /. 
    Automatic :> First[OptionValue[f, opts, {"newopt"}]]
   ];

Now let's see what happens:

f[]
f["newopt" -> 2]
f["oldopt" -> 3]
f["newopt" -> 4, "oldopt" -> 3]
f["oldopt" -> 3, "newopt" -> 5]

1

2

3

4

5

So, this always gives preference to values for "newopt" over values for "oldopt".

Note that we rely on the fact that OptionValue will treat OptionValue[f, opts, {"newopt"}] as before. So this will only work if you call OptionValue["newopt"], not if you request it with several option values at once like in OptionValue[{"opt1", "opt2", ... "newopt", ... }]. One might be able to make it work by specifying an additional rule à la

f /: OptionValue[
 f, 
 opts_, 
 list_List?(x \[Function] Length[list] >= 2 && MemberQ[list, "newopt"])
 ] := ...

This may be more work than OP was asking to do, so in that respect it might not be an answer to the question. I think that if you are trying to do something that requires adding definitions to OptionValue or other such gymnastics, it's a good sign you should do something else.

I would simply define a replacement rule and a message,

General::mypackagedeprec = "Option name `1` is deprecated and will not be supported in future versions of MyPackage. Use `2` instead."

fixOptionNames[func_] :=    ReplaceAll[
        HoldPattern[Rule["oldOptionName", rhs_]] :> (
            Message[
                MessageName[ func, "deprec"], 
                "oldOptionName", 
                "newOptionName"
            ];
            "newOptionName" -> rhs
        )
    ]

And then I would replace all instances of

OptionValue[ MyFunction, options, "oldOptionName"]

with

OptionValue[ MyFunction, fixOptionNames[MyFunction] @ options, "oldOptionName"]

Or even better, if you use the paradigm of having MyFunction call Catch[ iMyFunction[...]] then you can perform the replacement at that point. It does involve manually going through and making adjustments, but that seems the price to pay for renaming an option.

I like the message because it means you alert the users to the new name, and then after a few releases you can redefine fixOptionNames[___] to Identity or remove it entirely, but if you want to accept the old name in perpetuity then you'll need to keep it around.