10
$\begingroup$

Packages are (obviously) written in terms of built-in Mathematica functions. But many (if not, most) of these functions have options, like Expand, Solve, Compile

While a package is active, the user might still be interested in adjusting the default value of certain options for these built-in functions by modifying SetOptions[...]. Unfortunately doing this also (inadvertently) changes the internals of how package functions operate, and may lead to incorrect behavior.

What can I do as a package developer to effectively freeze the (unspecified) options of all built in package functions to their default values, so that they cannot be modified by SetOptions[...]?

Right now, I am in the process of going through my entire source code, and explicitly setting every possible option in every occurrence of every single built-in function to their system defaults. I am already 2 weeks into this, and made only 30% progress. It's also making the source code excessively verbose and difficult to read. A solution without having to do this would be most appreciated.

$\endgroup$
  • 2
    $\begingroup$ One possibility is to cache the default options somewhere and then use Internal`WithLocalSettings[] to set the environment in which the default options are always used. See e.g. this. See this as well. $\endgroup$ – J. M. is away Jul 28 '17 at 4:23
  • $\begingroup$ Related: (22697) $\endgroup$ – Mr.Wizard Jul 29 '17 at 9:23
13
$\begingroup$

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.

$\endgroup$
  • 2
    $\begingroup$ Nice abstraction. One niggle: you appear to be caching options for the entire System context. Is that really necessary? Couldn't you cache only those Symbols used? $\endgroup$ – Mr.Wizard Jul 29 '17 at 9:45
  • $\begingroup$ @Mr.Wizard Thanks. Re: caching - yes, sure, that's a possible optimization. The patch function then would have to make 2 passes: first to collect all the system symbols used, and then cache them, and then patch the package symbols on the second pass. $\endgroup$ – Leonid Shifrin Jul 29 '17 at 10:02
4
$\begingroup$

I really cannot compete with Leonid on metaprogramming and package development but I find the question interesting so I'd like to share a few thoughts.

You asked What can I do as a package developer to effectively freeze the (unspecified) options of all built in package functions and Leonid's method does this, however I am not sure that's a good idea in general. For example a user may wish to set certain display related options for better readability due to poor eyesight, and these should also affect your package function output where possible. Or options may be set to avoid over-consuming system resources, and again these should be respected to the extent possible.

We could make safe specific appearances of certain Symbols by applying a function to them. I'll name it safe.

mem : safe[s_Symbol] := mem =
  With[{ss = Symbol["safe`" <> SymbolName @ Unevaluated @ s]},
    Attributes[ss] = HoldAllComplete;
    (ss[arg___] := s[arg, ##]) & @@ 
       First @ ParallelEvaluate @ Options @ s;
    ss
  ]

Now when we need a function foo to act "normally" we can simply use safe[foo] in its place. This evaluates to safe`foo which carries a memoized definition that injects the default options. (Extracted using Jens's method from 60402 as Leonid did.)

$\endgroup$
  • $\begingroup$ Nice lightweight method. The only possible issue here is the creation of additional symbols, but this is a minor one. If that becomes a problem, one can use the version of your code returning pure function instead of a new symbol. +1. $\endgroup$ – Leonid Shifrin Jul 29 '17 at 16:27
  • $\begingroup$ @Leonid Thank you for your review. What do you think of the idea of adding MakeBoxes[ss, _] := ToBoxes @ Interpretation[s, ss]; to this? It would make it transparent in some ways, but would that be good or confusing? $\endgroup$ – Mr.Wizard Jul 29 '17 at 17:13
  • $\begingroup$ Not sure about that. It may end up more confusing than helpful. $\endgroup$ – Leonid Shifrin Jul 29 '17 at 17:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.