5
$\begingroup$

I came up with a construct that works well enough for my current use case but has some flaws (at least I suspect it does). Say we have a function that calculates some stuff that takes a while to process (nonlinear constrained optimisation in my case) and has some primary output (e.g. best fit parameters) but some secondary outputs that might be interesting in themselves or for diagnostics as well (e.g. a plot of the residual errors). Returning an Association containing the different outputs and thus making them accessible seems a good approach to me.

Now let's say some of those outputs are computationally expensive and are not generally needed when all goes well and the primary result looks fine right out of the box. In that case we would like to avoid the cost of calculating the secondary output(s), but being able to generate them at will would be nice nonetheless. The construct I used is:

ClearAll@func;

func[a_] := Module[{primaryOutput, secondaryOutput},
primaryOutput = (Pause@5; 2 a);
secondaryOutput := (Pause@2; 0.5 a);
Association["1" -> primaryOutput, "2" :>  secondaryOutput]]

Please note the use of RuleDelayed and SetDelayed insides Module and let's test what happens when we call that thing.

(test = func[1]) // AbsoluteTiming
(* {5.00136, <|"1" -> 2, "2" :> secondaryOutput$28323|>} *) 

test["1"] // AbsoluteTiming
(* {3.*10^-6, 2} *)

test["2"] // AbsoluteTiming
(* {2.00089, 0.5} *)

As we see from the timings, all looks good in principe. The general call of the function takes approx. five seconds (which is the duration of our primary "calculation") and no time is spend on the optional calculation of the secondary output. Accessing the result (with the "1"-key) is instantaneous. Accessing the secondary output (with the "2"-key) takes approx. two seconds (instead of seven) thus unnecessary recalculation of the primary output is avoided.

What I suspect might be an issue here is the secondaryOutput$28323 returned inside the resulting association. A new module variable is create at each call of func and I wonder both about proper scoping and garbage collection of these module variables. So the question is wether there is some merit to my suspicion and if so, how to fix this.

Bonus question: Since calculating the secondary output is expensive one might like to cache it using memoisation. Does this worsen the possible issue with those module variables?

$\endgroup$
6
  • $\begingroup$ You don't need to worry about those variables. As soon as they are no longer referenced by anything (like when you no longer use the resulting assoc in any other place), they will be GCd. So, to me, this is a non-issue, unless you have millions of such assocs in use at the same time. $\endgroup$ Jun 9, 2016 at 8:32
  • $\begingroup$ @LeonidShifrin I don't expect to handle a lot of those variables. Could you, in the light of Kuba's answer, elaborate on the mechanics of garbage collection? Kuba mentions that those module variables have the Temporary-attribute. $\endgroup$
    – Sascha
    Jun 9, 2016 at 19:18
  • $\begingroup$ You don't have to worry about them. Module assigns Temporary attribute to them automatically. They are collected after no longer referenced, this is also automatic. The only cases I know of, which are problematic, I discussed in this answer. So, basically, if your use case does not involve any of those subtle cases, just use those Module variables and don't worry about GC (don't forget set $HistoryLength to zero when working in the FrontEnd). $\endgroup$ Jun 9, 2016 at 21:00
  • $\begingroup$ Yep, maybe I should add I'm avoiding them just in case, I don't know internals enough and better be safe than sorry. @LeonidShifrin here's another case, which I don't know if is for sure Temporary fault but a "conservative" approach works stable: mathematica.stackexchange.com/q/116117/5478 $\endgroup$
    – Kuba
    Jun 10, 2016 at 6:58
  • $\begingroup$ @Kuba It is hard for me to say what's wrong in the example you linked, from a quick look, but my feeling is that the problem is hardly in Temporary attributes (although I may well be wrong). Something to look into, when I get a spare moment. But normally, Module -generated variables captured by UI elements are simply considered referenced from the GC, and work just fine within a single Mathematica session - at least this has been my experience. $\endgroup$ Jun 10, 2016 at 9:17

1 Answer 1

4
$\begingroup$

I'd not rely on Module variables after Module's evaluation is done. They have Temporary attribute and it can surprise you later.

One way around is to inject Own/DownValues:

func[a_] := Module[{primaryOutput, secondaryOutput}
  ,
  primaryOutput = (Pause@5; 2 a);
  secondaryOutput := (Pause@2; 0.5 a);

  Association[
     "1" -> primaryOutput, "2" :> secondaryOutput
  ] /. OwnValues[secondaryOutput]
]

now

(test = func[1]) // AbsoluteTiming

gives

{4.9994, <|"1" -> 2, "2" :> (Pause[2]; 0.5 1)|>}

In order to cache the second value you can do something like:

ClearAll@func;
SetAttributes[func, HoldFirst];

func[var_, a_] := Module[{primaryOutput, secondaryOutput}
  ,
  primaryOutput = (Pause@5; 2 a);
  secondaryOutput := var["2"] = (Pause@2; 0.5 a);
  var = Association["1" -> primaryOutput, "2" :> secondaryOutput] /. 
    OwnValues[secondaryOutput]
  ]


(func[test, 1]) // AbsoluteTiming
{4.99903, <|"1" -> 2, "2" :> (test["2"] = (Pause[2]; 0.5 1))|>}
test["2"] // AbsoluteTiming
{1.99965, 0.5}
test
<|"1" -> 2, "2" -> 0.5|>
$\endgroup$

Your Answer

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

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