21
$\begingroup$

I define functions a ton. In the past, I've mostly just defined my functions like:

myFunc[var1_]:=(
  var2=var1^2;
  Return@var2;
)

And I haven't run into too much trouble. If I wanted to be really careful for some applications, I might use Clear[] to clear a variable I'm about to use.

I'm now starting to use Module like so:

myFunc[var1_]:=Module[{var2},
  var2=var1^2;
  Return@var2;
]

But it doesn't seem that necessary in this short example, because var2 is immediately being assigned a value anyway.

Is it good practice to always use Module when defining functions?

$\endgroup$
  • 11
    $\begingroup$ The problem is not so much that when not localized, var2 may be in danger inside your function (which may also happen if var2 has a global value and you are not careful), but that you export this variable with its value into global namespace, after you call your function, and thereby pollute that namespace. When the code base grows larger, sooner or later this will cause trouble in some completely unrelated part. So, yes, do use Module or With for cases like that - this is exactly what they are for. $\endgroup$ – Leonid Shifrin Sep 21 '16 at 20:35
  • 1
    $\begingroup$ Whenever possible, use With. Use Block and Module if you know why you need it (Module if you need to modify a variable multiple times, Block if you need to undefine something). $\endgroup$ – masterxilo Sep 22 '16 at 1:41
27
$\begingroup$

The short answer is "yes", we should always use Module to localize any intermediate variables within our functions.

We might get away with using global variables for everything in an ad hoc, interactive session. But as we accumulate function definitions that we wish to re-use repeatedly over time, the chances of this practice causing problems increases dramatically.

To see this, let's consider a small scenario.

We start by defining a function:

myfunc1[x_] :=
  ( t1 = 3 x
  ; t2 = 2 x
  ; {t1, t2}
  )

myfunc1[10]
(* {30, 20} *)

Some time later, we define another function:

myfunc2[x_] :=
  ( t1 = {4 x}
  ; t2 = {3 x, 2 x}
  ; Join[t1, t2]
  )

myfunc2[10]
(* {40, 30, 20} *)

Later still, we realize that the computation of t2 within myfunc2 is just the same as the computation performed by myfunc1. We decide to change myfunc2 to use myfunc1 to ensure that these functions stay in synch as we make changes:

myfunc2[x_] :=
  ( t1 = {4 x}
  ; t2 = myfunc1[x]
  ; Join[t1, t2]
  )

Alas, this will give us a strange and unexpected result:

myfunc2[10]
(* Join[30, {30, 20}] *)

What happened? The problem is that both myfunc1 and myfunc2 use the same global variable t1 for intermediate results. myfunc2 initially sets t1 to the expected value ({40}), but myfunc1 subsequently smashes over that value with its own intermediate result (30). Join expects two lists but gets confused when its first argument is the integer 30.

Bugs likes this are insidious. In tiny examples like this it can be easy to find and fix the conflicting variable (t1), but once the number of functions grows it can become very difficult indeed. Furthermore, we need to come up with an ever-growing list of unique global variable names to hold intermediate results.

Here we were lucky: the answer was manifestly incorrect. But imagine now that the expressions in question were complex numeric computations. What if the answer were just out by a little bit? Would we even spot the problem? And if we did, what if it were only after a large number of iterations of a long chain of function calls? Which function(s) in that chain had variable conflicts?

The remedy, of course, is to use Module so that our intermediate variables do not conflict:

myfunc1[x_] :=
  Module[{t1, t2}
  , t1 = 3 x
  ; t2 = 2 x
  ; {t1, t2}
  ]

myfunc2[x_] :=
  Module[{t1, t2}
  , t1 = {4 x}
  ; t2 = myfunc1[x]
  ; Join[t1, t2]
  ]

myfunc2[10]

(* {40, 30, 20} *)

Using Module, we can name our intermediate variables whatever we like without having to keep track of whether we have ever used those names somewhere else.

Even though I said earlier that we might be able to get away without using Module during ad hoc interactive usage, I suggest that we should get into the habit of using Module routinely. An ad hoc session might very well be long and productive, involving the creation of many functions. The chance of conflict, and burden of coming up with unique names, both grow along with the number of functions. And one never knows whether any particular function might turn out to be worth keeping around for a long time (e.g. added to our init.m file, or some package we create).

$\endgroup$
  • $\begingroup$ Great answer. I feel happy for my decision to not expand my comment, since I probably wouldn't be able to articulate this so well. $\endgroup$ – Leonid Shifrin Sep 22 '16 at 1:31
  • $\begingroup$ What is the DISadvantage of using Module? Unless your function consists of a single line of code containing a single expression that calculates the single result of the function, Module can only make the purpose and implementation of the function clearer. $\endgroup$ – Ralph Dratman Sep 28 '16 at 5:38
  • $\begingroup$ @RalphDratman I do not think there are any disadvantages to using Module in the present context which is concerned with a change in imperative coding style away from global variables towards using local variables. A wider discussion might need to consider issues such as the performance impact of Module if used in an application hotspot, or Module's reduced relevance if one transitions to a less imperative and more functional or concatenative coding style. However, I suggest that such wider discussion is out of scope for the present question. $\endgroup$ – WReach Sep 28 '16 at 16:08
10
$\begingroup$

Disclaimer

The following is non-standard, probably contentious and possibly not apt for scenarios I haven't encountered but I have found the simplification this practice introduces so useful for building large code bases that I thought I'd share it.


I now never use With, Module and only v. occasionally use Block. It's all the way with Let.

myFunc[var1_]:=Let[
  var2=var1^2,
  var2
 ]

 myFunc[2]
(* 4 *)

For mine, this is cleaner, shorter and more expressive and an idiom that seems to serve all my encapsulation needs (including with Dynamic). It also has the advantage of naturally handling the very common case of both cascading local definitions

 myFuncC[var1_] := Let[
     var2 = var1^2,
     var3 = var2^2,
     {var2, var3}
    ]

 myFuncC[2]
 (* 4, 16 *)

as well as simultaneous local definitions

myFuncS[var1_] := Let[
     {varA, varB} = {var1^2, var1^3},
     foo[varA, varB]]

myFuncS[2]
(* foo[4,8] *)

Well the latter two use-cases were the original motivation behind Let's definition so it is of course really With (and variants) in disguise but even this extra layer on top of With comes in handy for implementing "dynamic debugging" and in unit testing by adding hooks into Let's definition.

I've adopted Let for awhile now without any issues at all but for all responsibility and maintenance take it up with the original author - Leonid :)

Very occasionally I'll use Block when needing to block a previous definition or preserve an old data structure but I shy away from any global idiom not only because of WReach's answer in describing how old values can unexpectedly intrude but also because in basic debugging/refactoring the cognitive overhead quickly mounts by having to follow more convoluted control flows.

The one exception is in building complex dynamical interfaces where the notion of a global variable makes more sense but in this case due to Dynamic/DynamicModule's semantics, IMO, an entirely new idiom using Let is needed.

The supposed use-cases for Module (changing local variables, defining local functions) I don't find persuasive. Nested changing of local variables IMO is better handled with an explicit recursive function while I don't bother with local functions since I find it makes for convoluted, less-maintainable code (not even mentioning the identified leakage, scoping and Dynamic issues). Smaller, helper functions called in Let in my experience can be more conveniently defined outside its scope.

I find these non-local helper definitions perfectly safe in an interactive session for initial experimentation/prototyping (where function-colliding is much less likely than variable-colliding). By the time function-colliding might become an issue, the code has long migrated into a package where the helpers sit safely encapsulated and ready for "public usage" if broader use is subsequently required. RIP Module.


Aside

Well OK, there is a certain situation where I need to resurrect Module. It is when a local data structure needs to be generated as in the following pseudo(ish) code.

f[a_, b_] = someHelperFunction[a, b];

 myFuncP[args___] := Module[
 {lds = GenerateLocalDataStructure[args]},
 Let[
     a = preProcessing1@args,
     b = preProcessing2@args,
     c = somePosition@args,
     \[FilledSquare];
     lds = MapAt[f[a, b], c]@lds
 ]]

Note this involves the only wrinkle with Let that you sometimes have to be aware of - without \[FilledSquare] Let thinks it owns lds. The ; tells Let, that its local defining is over and hence that lds actually belongs to Module. (hence \[FilledSquare] is just a dummy expression - it could be anything followed by a ; - its chosen here for code readability).

Note also, that this wrinkle doesn't arise if there was a way of (mutably) modifying a datastructure that didn't use Set - see these comments. This IMO is another argument for the practice for not overloading Set where reasonable (as suggested by Leonid in that discussion) and another use for the introduction of something like @=. With the latter defined in a .m file no \FilledSquare is needed and in my set up the above looks like:

myFuncS[args___] := Module[
  {lds = GenerateLocalDataStructure[args]},
  Let[
   a = preProcessing1@args,
   b = preProcessing1@args,
   c = somePositionInlds@args,
   lds @= MapAt[f[a, b], c]
 ]]

WL's functional nature means that local variables tend to arise arise more often that local data structures. Largish data structures typically get passed in as a (held) argument and therefore with scoping that Let can't touch. This is perhaps why, in my experience, this use-case of semi-persistent, local data structures rarely arises. RIPurgatory Module.

$\endgroup$
  • $\begingroup$ +1... on the topic of scoping macros, I'll throw in a pitch for withSetup from this answer ;) $\endgroup$ – WReach Sep 22 '16 at 12:18
  • $\begingroup$ @WReach I had wondered what drawbacks their might be to this holus-bolus adoption of Let (hence slightly trolling answer) including perhaps memory management. While your withSetup was designed to enhance Module's memory management I was immediately struck by the potential upside from processing local variables behind the scenes (i.e. not just the debugging mentioned in the my answer but also $\endgroup$ – Ronald Monson Sep 23 '16 at 1:29
  • $\begingroup$ memory management/profiling etc). I've often thought that the whole process of profiling/regression testing/coverage in Mma could be automated using lexical information that already exists. Having the cleanest, easiest, way to do sequential/multiple assignments encourages a very fine granularity needed in large code bases (as previously discussed by Mr. Wizard/Leonid) which in turn would facilitate this automation by adding such structured information. So yes a broader pitching ... :) $\endgroup$ – Ronald Monson Sep 23 '16 at 1:36

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.