23
$\begingroup$

Is there any way to "close" a package (or a symbol, or a context) in that if a user of the package adds definitions to the symbol they will be tried before the package defined ones, just like what happens with built-ins?

$\endgroup$
14
  • 1
    $\begingroup$ This is a very important question. This will allow users to wrap package functions. Furthermore, certain "builtins", such as all Parallel* functions, are actually implemented as a package!! $\endgroup$
    – Szabolcs
    Mar 14, 2012 at 19:37
  • 1
    $\begingroup$ @Szabolcs, but in those cases their functions don't work "as regular built-ins". Try adding ParallelSubmit[___]:=8 after unprotecting... It gets added at the bottom of the DownValue list and ParallelSubmit[2+2] works "as usual" instead of catching the new definition $\endgroup$
    – Rojo
    Mar 14, 2012 at 22:27
  • $\begingroup$ @Rojo Works with my method for a double blank (although with a slight glitch, an error message is generated - due to a minor bug, but that does not affect the result). Try withUserDefs[ParallelSubmit, {SetAttributes[ParallelSubmit, HoldAllComplete]; ParallelSubmit[__] := 8}, ParallelSubmit[2 + 2]]. Although, tripple blank is the only pattern which my method does not currently cover. $\endgroup$ Mar 15, 2012 at 0:09
  • $\begingroup$ @LeonidShifrin, your method is great, robust. But It is for cases when you're the user of a package and want to extend it. The other answers are attempts to deal with the case when you are creating the "extensible" package. What I just commented here was that the Parallel* built-ins don't behave as "closed" like other built-ins. You can extend them using your magic, but not right away. So, peeking at how they did it won't help improve those attempts $\endgroup$
    – Rojo
    Mar 15, 2012 at 0:15
  • 1
    $\begingroup$ @Rojo I think, Szabolcs didn't mean to peek at how those were written - he just meant that it would be nice to be able to consistently override functions, no matter built-ins or top-level. My mechanism is not perfect, but possesses one important property - it can be nested. Nesting is important, because it gives some level of consistency. $\endgroup$ Mar 15, 2012 at 0:24

4 Answers 4

16
$\begingroup$

I will suggest a solution for DownValues - based definitions, but it may be generalized to other types of definitions as well. I will only consider a case of a single symbol, but again, the generalization is rather straightforward. You will also have to execute your code in a special dynamic environment.

A first ingredient of my suggestion is a (slightly modified) symbol-cloning functionality described here:

Clear[GlobalProperties];
GlobalProperties[] := 
 {OwnValues, DownValues, SubValues, UpValues, 
    NValues, FormatValues, Options, DefaultValues, Attributes};

ClearAll[clone];
Attributes[clone] = {HoldAll};
clone[s_Symbol, new_Symbol] :=
  With[{clone = new, sopts = Options[Unevaluated[s]]},
    With[{setProp = (#[clone] = (#[s] /. HoldPattern[s] :> clone)) &},
      Map[setProp, DeleteCases[GlobalProperties[], Options]];
      If[sopts =!= {}, Options[clone] = (sopts /. HoldPattern[s] :> clone)];
      HoldPattern[s] :> clone]]

Here comes my suggested dynamic environment then:

ClearAll[withUserDefs];
SetAttributes[withUserDefs, HoldAll];
withUserDefs[sym_Symbol, {defs__}, code_] :=
  Module[{s, inSym},
    clone[sym, s];
    Block[{sym},
     defs;
     sym[args___] /; ! TrueQ[inSym] :=
        Block[{sym, inSym = True},
          clone[s, sym];
          With[{result = sym[args]},
             result /; result =!= Unevaluated[sym[args]]
          ]
        ];
     code]];

What is happening here: first, we clone the original symbol. Then, we Block it, and run the definitions which we want to override the previous ones. Then, we add a catch-all definition which uses the Villegas-Gayley trick, but also Block-s the symbol in question again, and inside the inner Block uses the clone to effectively "unblock" the symbol to its original defs by reverse-cloning it, and run those. The extra trick to use With and a shared local variable result is needed to avoid infinite iteration in cases when neither user-defined nor previous rules apply.

Here comes an example:

ClearAll[f];
f[1] = 10;
f[x_] := x^2;
f[x_, y_] := x + y;

And now:

withUserDefs[f, {f[x_] := x^4}, {f[1], f[2], f[1, 2], f[1, 2, 3]}]

(* 
  ==> {1, 16, 3, f[1, 2, 3]}
*)

you can see that in the first two cases, the modified definitions were used, in the third one the original definition was used, and the last one did not match any and evaluated to itself.

One can nest these constructs, and the inner one will override the outer ones then. For example:

withUserDefs[f, {f[x_] := x^4}, 
   withUserDefs[f, {f[1] := 100}, 
     {f[1], f[2], f[1, 2], f[1, 2, 3]}]]

(* 
  ==> {100, 16, 3, f[1, 2, 3]}
*)

You can ask why I wasn't just using the Villegas-Gayley trick by itself, which is much simpler. The answer is that there is no guarantee that the ordering of definitions will be right with it, even if we manually reorder them, and moreover, cases like f[1] = 10 are immune to all reorderings of DownValues and will always be at the top, so can not be dealt with at all, within a pure Villegas-Gayley approach - but can be successfully dealt with in this more complex one.

The suggested approach is as good as the symbol's cloning procedure is. For SubValues, UpValues etc, it should be modified. The full solution involving all possible global rules and multiple symbols will likely look more complex, but I just wanted to illustrate the idea in the simplest possible setting. Also, we probably can not count on such generalization to be fully robust.

$\endgroup$
2
  • 2
    $\begingroup$ It's not a System`​ symbol, but Language`ExtendedDefinition is very useful for cloning. It is used extensively in the cloud deployment of Wolfram Language, so should be safe for users to use as well. It exists at least as far back as M9. $\endgroup$
    – Carl Woll
    Aug 17, 2017 at 19:26
  • $\begingroup$ @CarlWoll Thanks. I've been aware of it for a while now. Actually, learned about it from this discussion. But not at the time when I was writing this answer. Should've updated it. $\endgroup$ Aug 17, 2017 at 19:39
14
$\begingroup$

If we are the ones writing the package in question, then we could proceed as follows. First, we define a public version of the function that delegates all calls to a private version:

ClearAll[publicFn]
publicFn[args___] := privateFn[args]

Then we define the functionality we desire on the private function:

ClearAll[privateFn]
privateFn[x_] /; x < 0 := 100 x
privateFn[x_] := 10 x

Before users make any modifications, the public function behaves in the way we have defined:

publicFn /@ Range[-5, 5]

{-500,-400,-300,-200,-100,0,10,20,30,40,50}

Now, if a user comes along and adds a definition to the public function...

publicFn[n_?EvenQ] := 0

... then the corresponding behaviour is overridden, but the other definitions remain in place.

publicFn /@ Range[-5, 5]

{-500,0,-300,0,-100,0,10,0,30,0,50}

Since the public function is defined with the broadest possible definition, all more-specific user definitions will be tried before the pre-defined rules. Of course, if the user redefines the public function's one definition exactly, then all bets are off.

Handling Partial Functions

If the function is only partially defined, we may want the public function to return unevaluated when the arguments are outside its domain. To do this, we need a slightly more elaborate version of the public function:

ClearAll[publicFn]
publicFn[args___] := Module[{v = privateFn[args]}, v /; !MatchQ[v, _privateFn]]

Unfortunately, Mathematica no longer considers this definition to be "broad" due to the complex MatchQ condition. Consequently, this definition will no longer be applied after subsequent user-supplied definitions. What we need to do is take over the management of the down-values of publicFn so that our "broad" definition is always last in the list. This is accomplished by installing a new SetDelayed/Set definition as an up-value on publicFn:

publicFn /: (SetDelayed|Set)[h_publicFn, def_] :=
  (DownValues[publicFn] =
    Insert[
      DeleteCases[DownValues[publicFn], Verbatim@HoldPattern[h] :> _]
    , HoldPattern[h] :> def
    , -2
    ];)

We'll change privateFn so that it is only partially defined:

ClearAll[privateFn]
privateFn[x_] /; x < 0 := 100 x

publicFn /@ Range[-5, 5]

{-500, -400, -300, -200, -100, publicFn[0], publicFn[1], publicFn[2], publicFn[3], publicFn[4], publicFn[5]}

Users can then add further partial definitions:

publicFn[n_?EvenQ] := 0
publicFn[5] = "will be replaced by a subsequent definition";
publicFn[5] = "five";

{-500, 0, -300, 0, -100, 0, publicFn[1], 0, publicFn[3], 0, "five"}

$\endgroup$
6
  • $\begingroup$ Nice one +1 Any way to allow our package function to return unevaluated (publicFn[..]) when it doesn't match the definitions we gave? $\endgroup$
    – Rojo
    Mar 11, 2012 at 1:12
  • $\begingroup$ @Rojo Yes, see the new section Handling Partial Functions. $\endgroup$
    – WReach
    Mar 11, 2012 at 1:55
  • $\begingroup$ but that last one works because privateFn fails with even arguments. If I remove the negative condition the user defined version never gets a chance to overload $\endgroup$
    – Rojo
    Mar 11, 2012 at 1:59
  • $\begingroup$ @Rojo You are correct -- user definitions are ignored in the new version. The problem is that the condition in the new definition of publicFn causes Mathematica to regard the definition as specialized instead of broad. Consequently it is no longer applied after all other definitions. I'm withdrawing the section on partial functions until such time that I have an alternative solution. Assuming that time ever comes... :) $\endgroup$
    – WReach
    Mar 11, 2012 at 6:01
  • 1
    $\begingroup$ Thanks WReach. With your and @MrWizard's ideas I think I came up with a way that doesn't require resorting DownValues or defining upvalues on set and posted it. $\endgroup$
    – Rojo
    Mar 13, 2012 at 3:23
8
$\begingroup$

Here is method based on WReach's answer. It works by explicitly sorting the list of DownValues after any new assignment is made, placing the ___ last. It allows the return the public function unevaluated. By nature this only works for DownValues definitions.

ClearAll[func, privateFn]

privateFn[x_] /; x < 0 := 100 x
privateFn[x_?NumericQ] := 10 x

func[args___] := With[{eval = privateFn[args]}, eval /; ! MatchQ[eval, _privateFn]]

func /: (set : Set | SetDelayed)[lhs_func, rhs_] /; ! TrueQ[$deffunc] :=
  Block[{$deffunc = True},
    lhs ~set~ rhs;
    DownValues[func] = 
     SortBy[
       DownValues[func],
       {MatchQ[ #[[1]], _[_[_[_, Verbatim[___]]]] ] &}
     ];
  ]

Testing:

func[n_Integer?EvenQ] := 0
func[n_Integer] := 1

func /@ Range[-5, 5]
func[0.7]
func["undefined"]
{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}

7.

func["undefined"]
$\endgroup$
4
  • 8
    $\begingroup$ _[_[_[_, Verbatim[___]]]] that's the scariest pattern I've ever seen. (note I said seen, not understood) $\endgroup$
    – Rojo
    Mar 13, 2012 at 3:23
  • 8
    $\begingroup$ @Rojo is _~_~Verbatim@___//_//_ more cheerful? :o) It's just a pattern to match HoldPattern[func[args___]] :> something where args is arbitrary. $\endgroup$
    – Mr.Wizard
    Mar 13, 2012 at 6:06
  • 2
    $\begingroup$ Haha, ok, I'm glad you used the first version $\endgroup$
    – Rojo
    Mar 14, 2012 at 23:43
  • 2
    $\begingroup$ Just saw this. +2 for the pattern, -1 for making my head hurt first thing in the morning… ;P $\endgroup$ May 30, 2015 at 1:12
6
$\begingroup$

Ok, based on your answers...

ClearAll[publicFn]
Module[{guard = True},
 publicFn[args___] /; guard := 
  Block[{guard = False}, Module[{res = publicFn[args]},
    If[MatchQ[res, _publicFn],
     res = privateFn[args]]; res /; ! MatchQ[res, _privateFn]]
   ]
 ]

This doesn't suffer problems of the automatic DownValue resorting, and without resorting to upvalues on Set functions... Shouldn't have problems with UpValues either. The public definition of the function is found perhaps before or perhaps after some user defined definitions. But if it is found, it makes sure there aren't any other good public definitions before transferring the job to the private definition. If there are none, it returns unevaluated.

privateFn[x_] /; x < 0 := 100 x
privateFn[x_?NumericQ] := 10 x
privateFn[3] = 99

publicFn /@ Range[-5 , 5]
publicFn["oij"]


{-500, 0, -300, 0, -100, 0, 10, 0, 30, 0, 50}  
publicFn["oij"]

Now, making some definitions

publicFn[n_?EvenQ] := 0;
publicFn[3 | 33] = -100;
soh /: publicFn[soh] := "boo"

We see that

publicFn /@ Range[-5 , 5]
publicFn["oij"]
publicFn[soh]

returns

{-500, 0, -300, 0, -100, 0, 10, 0, -100, 0, 50}

publicFn["oij"]

"boo"
$\endgroup$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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