last day (15 days later) » 

08:08
9
A: Garbage collection for lexical closures

Lukas LangHere's an approach to "automate" the usage of the ExpressionCleanup` paclet mentioned by @LeonidShifrin. The idea is to return an object from your module that contains two symbols: One for the actual downvalues (f), and one purely for keeping track of the reference count (marker). It is importan...

Very nice proposal! I wonder what disadvantages it has? Loading the paclet is minor. The fact that M does not evaluate to a symbol is, I guess, mostly minor. Using SubValues of f rather than DownValues may have an impact on how quickly the definitions are resolved by the Mathematica evaluator, but I am often not concerned about that. Any major disadvantages that I am missing?
I'm glad to see this get some use!
I have a version based on your marker idea, but which does not require a SubValues syntax, but instead uses Function: addGCMarker[funcSymbol_Symbol] := Module[{marker}, AddCleanupFunction[marker, Echo @ "Removed!"; Remove @ funcSymbol]; Function[marker;funcSymbol[##]]];. This allows one to define simply: getMultiplier[n_]:= addGCMarker @ Module[{f},f[x_] := n * x; f]. I also have a utility function to download / install the ExpressionCleanup paclet. I could edit this into your answer, if you don't mind. This would be better than updating mine, since mine uses a different idea.
@JasonB. I have starred this project of yours long time ago, with a plan to use it in some of my projects. This Q/A seems like a perfect application for it.
@LeonidShifrin: Beware that the lexical closure environment could contain symbols other than the f that is returned, for example auxiliary symbols that f refers to and that have their own DownValues. Would be great to have a solution that is flexible enough to cover that case also. Can that be done with a wrapper like yours?
@user293787 You will probably need to wrap addGCMarker around any local symbol with DownValues. Can be done also inside the main Module that defines f. That can probably be automated by some meta-programming which finds local dependencies. I will look into this. At the moment this is indeed a bit unwieldy: getMultiplier[n_]:= addGCMarker @ Module[{f, g, gFinal},f[x_] := n * gFinal[x]; g[x_] := x; gFinal = addGCMarker[g]; f] and manual.
08:08
@LeonidShifrin Thanks for the suggestion! Using Function is indeed a nice improvement, especially since we can even add attributes this way. I have updated my answer with a solution based on your idea
@user293787 I have added some updated versions, including some approaches to handle helper symbols
@LukasLang Very nice solution overall. But I think that, for this to be fully robust, the dependency tracker should be more sophisticated, to process possible nested Module- s. Also, I would try to perform the analysis at the point when addGCMarker is applied, not when AddCleanupFunction is called - although for this point I don't have a very strong opinion (in particular because one has more information available at run-time, compared to "definition-time", which might be important and is an argument in favour of your current approach).
@LukasLang Great work! I feel like the version that inspects the module directly is cleaner and perhaps more reliable (there can be auxiliary symbols h that g refers to but that are not referred to by f itself...) but I do not claim to understand it very well. On the other hand, what if vars also contains initializations?
@user293787 Language`ExtendedFullDefinition actually handles nested dependencies like you describe properly, so both approaches should be able to handle cleanup of such cases.
@LeonidShifrin Yeah, there's definitely room for improvement... Unfortunately, I couldn't think of a clean way to do so, so I thought I'd just present two different approaches that other people can tweak to their liking. If you have any ideas, I'd of course be very interested. I have one more crazy idea that I might try later, let's hope it leads somewhere...
@LukasLang I was thinking of something along the lines of this, modified to the needs of this problem (like not excluding defs inside Module - I have some comment for that part in that code), and also recording the depth of the Module to identify conflicting local vars. That is, for the "static" approach. If that can't be made fully robust, then the "dynamic" approach based on Language`ExtendedFullDefinition is probably the way to go, but I would try to be more selective there, while still accounting for nested Modules etc.
@LukasLang There might be a different scheme altogether, where one simply renames all relevant Module variables of inner Module-s in some way, during the defs processing stage (when the closure is created from "source" code), that would allow the cleanup to unambiguously remove them all based on some name pattern, that will be unique for a given closure, but not tied to a particular internal Module or their set.
@LukasLang One particular case that will badly fail within the current approach, is when the closure being defined, returns other closures as a result of its operation, and those other closures use some of the inner local symbols. In that case, removal of local symbols will make those returned closures invalid, whereas they could still be referenced / in use. So the problem is more complex, and a better approach would probably be to wrap something like addGCMarker around every local symbol that is found to have DownValues, SubValues or UpValues. That's where "static" analysis can help.
Thank you very much, @LeonidShifrin and @LukasLang. I like this solution because it allows one to add a GC to existing memory-leaking code by simply wrapping addGCMarker around affected modules, without otherwise modifying code. That is the best one can hope for a workaround. I admit that I have not yet tried to understand if all or most tricky cases are handled correctly, but I will start using it in my projects and find out. Your answer makes me wonder even more why the GC is not there in Mathematica already, given that closures are such a natural programming pattern in Mathematica.
Further comments, ideas, improvements are of course very welcome.
08:08
@user293787 I intend to revisit this later when I get more time. I think this is really important question / topic. But this solution certainly is a good starting point.
@LeonidShifrin I have added yet another approach, it should at least support nested Modules, and should also be nicer otherwise. Any feedback is of course welcome :)
@user293787 I have added yet another version, see the updated answer. If you need help understanding anything, do not hesitate to ask!
 
1 hour later…
09:29
Some comments about GCBlock (which I like very much) all with the reservation that I am new to Mathematica meta-programming, so please correct me if I misunderstand. Here is the doc for Inherited`Block for those not familiar with it, like myself.
I wrapped a Module[{$active},...] over the entire def of GCBlock for my piece of mind, I hope that does not break things.
So it now suffices to wrap GCBlock@ around the outermost Module and it will also add a GC to all Module inside, but only those evaluated immediately (?). If a module inside is constructed lazily as in z=GCBlock@Module[{f},f[1]:=f[1]=Module[{g},g[4]:=3;g];f]; then the inner Module has no GC. So one still needs to wrap GCBlock@ around all Module. So what stops one from redefining Module globally then?
I guess I should add @LeonidShifrin and @LukasLang. Thanks for your interest btw.
StringReplace[..., "piece of mind" -> "peace of mind"]
09:50
@user293787 that's shouldn't cause any issues, I just don't use it typically because rerunning the code will duplicate the definition (since the two definitions have different $active variables). But here, that's anyway irrelevant, because the definition will be removed after the Internal`InheritedBlock
@user293787 Yes, that is correct. I would be really hesitant to redefine something like Module globally, both due to performance concerns and because GCBlock slightly changes semantics in some cases (for instance, it doesn't work for stuff with UpValues, I have also updated makeGCed to account for that). Hence fthe Block-like solution that allows you to control when to use it
One option might be to "augment" Module uses inside of GCBlock automatically, using something like this:
Attributes[makeGCed] = {HoldFirst};
makeGCed[f_Symbol] /; OwnValues[f] === {} && UpValues[f] === {} :=
 Module[
  {marker, proxy},
  Language`ExtendedFullDefinition[] = Replace[
    Replace[
     Language`ExtendedDefinition[f] /. f -> proxy,
     HoldPattern@Module[args___] :> GCBlock@Module[args],
     All
     ],
    HoldPattern@GCBlock@GCBlock@Module[args___] :>
     GCBlock@Module[args],
    All
    ];
  ClearAll[f];
  Attributes[f] = {Temporary};
  AddCleanupFunction[f, Echo@"f removed!"];
(I'm using Replace[...,All] to handle nested Modules, and the second Replace is there to deduplicate GCBlock applications)
10:08
@LukasLang Thanks, that makes sense. From a practical perspective, I am not concerned about having to add GCBlock@ a few times. But I am surprised that one is very close to having constructed an actual GC if Module is redefined globally, which I understand is a bit dangerous (performance and conflicts).
 
5 hours later…
14:40
@LukasLang That's certainly a nice attempt, and it seems to go a long way towards the desired result (I mean the code you added to your answer). I am a little concerned about redefining Module (even with InheritedBlock), since this is done for all Module-s inside that part of execution stack, out of which only very few might be concerned with this particular closure. In other words, I would try to make things more lexical. This is not so hard to do.
@LukasLang It also seems to have a typo, I guess you meant Language`ExtendedFullDefinition[proxy] = ..., not Language`ExtendedFullDefinition[] = ....
Finally, there is this problem that symbols with both UpValues and DownValues are not handled in this approach, and I see why you want this behavior, but this certainly looks like a limitation that needs at least to be mentioned.
Otherwise, this looks very nice, and FWIW probably close to what I would've tried as well.
15:18
@LeonidShifrin No, that was intentional. The Language`DefinitionList contains symbol anyway, so the argument to Language`ExtendedFullDefinition seems to be ignored. (Since that function can anyway return definitions for several symbols, I like to keep the argument empty to avoid false impressions)
@LeonidShifrin good point, I'll add a more explicit note to the answer
16:02
@LukasLang Good point. That's an interesting tidbit, which I either forgot or actually did not know about these functions. I did find a reference on M SE on that.

last day (15 days later) »