The approach
This seems to be a good case for applying some run-time metaprogramming / introspection. The idea would be to patch the already constructed definitions after they have been created, at the end of the package. Basically, we can search for all system functions with options, present in the code, and replace them as
function[args___] :> function[args, Sequence @@ StandardOptions[function]]
(this is pseudocode).
To illustrate the approach, consider a simple package like the following:
BeginPackage["Test`"]
myFirstFunction;
mySecondFunction;
Begin["`Private`"]
ClearAll[myFirstFunction];
myFirstFunction[a_]:= Plot[Sin[a * x], {x, 0, 10}]
myFirstFunction[a_, b_]:= Plot[{Sin[a * x], Sin[b * x]}, {x, 0, 10}]
ClearAll[mySecondFunction];
mySecondFunction[a_]:= NIntegrate[ Exp[- a * Sin[x]^2], {x, 0, Infinity}];
End[]
EndPackage[]
Patching the code
Code
Here is the code that illustrates this approach:
BeginPackage["Test`"]
myFirstFunction;
mySecondFunction;
Begin["`Patcher`"]
ClearAll[getSystemSymbolsOptions];
getSystemSymbolsOptions[]:=
First @ ParallelEvaluate[
Module[{options = <||>},
Quiet @ Scan[
Function[
name,
ToExpression[
name,
StandardForm,
Function[
sym,
options[HoldComplete[sym]] = Options[Unevaluated[sym]],
HoldAllComplete
]
]
],
Names["System`*"]
];
options
]
];
ClearAll[options];
opts:=opts = getSystemSymbolsOptions[];
options[sym_] := Lookup[opts, HoldComplete[sym], {}];
ClearAll[patchDef];
patchDef[expr_]:=
Replace[
expr,
s_Symbol[args___] /; Context[s] === "System`" :>
With[{defaultOpts = Sequence @@ options[Unevaluated[s]]},
s[args, defaultOpts] /; {defaultOpts} =!= {}
],
{0, Infinity},
Heads -> True
];
ClearAll[patch];
patch[sym_Symbol]:=
Scan[
Function[prop, prop[sym] = patchDef[prop[sym]]],
{OwnValues, DownValues, SubValues, UpValues}
];
patch[symname_String]:=
ToExpression[
symname,
StandardForm,
Function[sym, patch @ Unevaluated @ sym, HoldAllComplete]
];
patch[names_List]:=Scan[patch, names];
End[]
Begin["`Private`"]
ClearAll[myFirstFunction];
myFirstFunction[a_]:= Plot[Sin[a * x], {x, 0, 10}]
myFirstFunction[a_, b_]:= Plot[{Sin[a * x], Sin[b * x]}, {x, 0, 10}]
ClearAll[mySecondFunction];
mySecondFunction[a_]:= NIntegrate[ Exp[- a * Sin[x]^2], {x, 0, Infinity}];
Test`Patcher`patch @ Names["Test`Private`*"]
Test`Patcher`patch @ Names["Test`*"]
End[]
EndPackage[]
Explanation
You can see that we have added a sub-context "`Patcher`"
to the test package, as well as two lines
Test`Patcher`patch @ Names["Test`Private`*"]
Test`Patcher`patch @ Names["Test`*"]
at the end of the package.
So, how does Test`Patcher`patch
work? We first collect the default options into the Test`Patcher`opts
private variable, using a trick from this answer. Then, the function Test`Patcher`patch
, when applied to a symbol, replaces its DownValues
, OwnValues
, SubValues
and UpValues
by their patched versions, and the patching is performed by the Test`Patcher`patchDef
function, which does a replacement similar to the one described above. We apply Test`Patcher`patch
to all symbols in the package's context and its ```Private` `` subcontext.
If you execute the above code, and then test, you can see that it worked:
?mySecondFunction
Test`mySecondFunction
mySecondFunction[Test`Private`a_]:=NIntegrate[Exp[-Test`Private`a
Sin[Test`Private`x]^2],{Test`Private`x,0 [Infinity]},
Sequence[AccuracyGoal->\[Infinity],Compiled->Automatic,
EvaluationMonitor->None, Exclusions->None,MaxPoints->Automatic,
MaxRecursion->Automatic,Method->Automatic,MinRecursion->0,
PrecisionGoal->Automatic,WorkingPrecision->MachinePrecision]]
Notes
The advantage of this approach w.r.t. approaches based in one way or another on SetOptions
is that here, we change things locally. Since we pass all relevant options explicitly, we neither care about global options settings for built-ins, nor need to change them in any way (through local dynamic environment or otherwise). This means, in particular, that two different packages can be patched in this way independently of each other, and then used simultaneously, without errors. Also, since we never change the global state of the system (SetOptions
), this approach is completely non-intrusive.
OTOH, the lexical nature of replacements in the suggested approach limits its introspective capabilities, and there are cases where it won't work.
One particular case where this approach will break is when the function calls are constructed dynamically at runtime, e.g. using Apply
, like for example in
...
result = If[doNumericalIntegration, NIntegrate, Integrate] @@ {args}
...
It might be possible to detect some of such cases (e.g. by watching calls to Apply
), but certainly not all.
There can be variations of this approach. For example, the functionality of the `Patcher`
subcontext can be moved to a separate package, which can be loaded privately with Needs
inside your package's private section. Also, instead of injecting the actual value of options, one could choose to inject options[sym]
(which won't then be evaluated until the function is called) - which may be a better option in certain cases (and the patched code then is not bloated with explicit options).
Right now it only patches the System symbols in the calls. One could also extend the patcher to patch symbols from other contexts - which will be necessary in general, if you use symbols from other contexts in your code, and are concerned about the same option-resetting issue for those contexts.
As mentioned, one can construct cases where this approach will break, but I think that it can handle most common use cases, and can perhaps be modified and / or extended to fit one's more special needs.
Internal`WithLocalSettings[]
to set the environment in which the default options are always used. See e.g. this. See this as well. $\endgroup$