Functions with changeable global variables

Introduction

The question is: how to correctly organize all this?

Of course, there are many ways to answer the question, ranging from re-education suggestions to click-through paths in a relevant IDE.

The main conflicting forces behind these kind of software design questions (as the one in this discussion) are:

  • using global variables is convenient, and

  • using global variables can make code hard to read and full of bugs.

Below are given several styles that provide a compromise.

Answers to EDIT 2 of the question

Can anybody explain in simple way (understandable for novice) why globals and code like mine are not recommended in MMA? Not just for the reason that advanced users of WL are not used to do things this way, but what is really wrong, what may lead to errors.

With the standard software engineering goals (not "novice" ones) using global variables is bad in any language not just Mathematica / WL.

Of course, if the code is short and/or is a one-off affair, run in a notebook, then global variables are fine.

For code that is under development or it is supposed to be developed further global variables are bad mainly because:

  • they prevent from reasoning effectively about the functions definitions, and

  • the state of execution is unpredictable -- any part of the code can change the global variables at any time.

There are other reasons and possible coding styles and remedies that can be classified from different perspectives. See for example:

  • "Global Variables Are Bad", or

  • the answers of "Why is Global State so Evil?" at softwareengineering.stackexchange.

The links above give answers to the request:

[...] Not just for the reason that advanced users of WL are not used to do things this way, but what is really wrong, what may lead to errors.

In general, since Mathematica / WL is mainly a functional language one is better off undertaking longer programming endeavors without relying on side effects during function execution (global state change or using Sow).

Suggested code changes

Using a context

The minimal effort way (i.e. rarely a good one) to somewhat contain the possible bugs is to move all function definitions and global variables into a context.

Monads(-like)

I would say the simplest thing to do in the direction of "doing it right" is to add the global variables as an argument to all functions and as a result element to all functions. (See this related discussion.)

With this approach all of the functions are defined to have the type (domain->codomain):

{args__, params_List} -> {result_, newParams_List}

or

{args__, params_Association} -> {result_, newParams_Association}

For example:

Clear[line]
(*line[l_,{V0_,W0_,mstraight_,size_}]:=line[l,{V0,W0,mstraight,size}];*)
line[l_, {V0_, W0_, mstraight_, size_}] :=
  Module[{q, V, W},
   V = W0.straight[l] + V0; 
   q = {RGBColor[0.5, 0.5, 0.5], CapForm -> "Butt", Tube[{V0, V}, size]}; 
   W = W0.mstraight; 
   {q, {V, W, mstraight, size}}
  ];

Remark: Note the commented out overloading of the function line to support your current signature -- it is better not to have it since the return result structure is different, but it might be an useful intermediate step during the code refactoring / transition.

A call to that function would be:

{res, newParams} = line[lVal, currentParams];

A further step in that direction is to use an Association in order to facilitate the management of the global parameters. For example:

Clear[line]
line[l_, params_Association] :=      
  Module[{q, V, W, V0, W0, size, mstraight},
   {V0, W0, size, mstraight} = params /@ {"V0", "W0", "size", "mstraight"};
   V = W0.straight[l] + V0; 
   q = {RGBColor[0.5, 0.5, 0.5], CapForm -> "Butt", Tube[{V0, V}, size]}; 
   W = W0.mstraight; 
   {q, 
    Join[params, 
     AssociationThread[{"V0", "W0", "size", "mstraight"} -> {V, W, 
        mstraight, size}]]}
  ];

Using named arguments and results

Following the suggestion in the previous section of using Association, instead of separating the function arguments into particular (l) and common (params), we can just use an Association to hold -- and name -- all arguments.

For example:

Clear[line]
line[args_Association] :=     
  Module[{l, q, V, W, V0, W0, size, mstraight},
   {l, V0, W0, size, mstraight} = args /@ {"l", "V0", "W0", "size", "mstraight"};
   V = W0.straight[l] + V0; 
   q = {RGBColor[0.5, 0.5, 0.5], CapForm -> "Butt", Tube[{V0, V}, size]}; W = W0.mstraight; 
   Join[args, 
    AssociationThread[{"Result", "V0", "W0", "size", "mstraight"} -> {q, V, W, mstraight, size}]]
  ];

Note the special key "Result".

Assuming glParams is an Association with the global parameters

glParams = <|"V0" -> 12, "W0" -> RandomReal[{0, 1}, {3, 3}], 
   "size" -> 200, "mstraight" -> {0, 0, 1}|>;

a call to that function would be:

glParams = line[Append[glParams, "l" -> 34]];
glParams["Result"]

(* {RGBColor[0.5, 0.5, 0.5], CapForm -> "Butt", Tube[{12, 
    12 + {{0.178045, 0.278631, 0.528348}, {0.344852, 0.57178, 
   0.0358229}, {0.693822, 0.454272, 0.93838}}.straight[34]}, 200]} *)

Remark: R supports this style of naming arguments and results in a direct way.

Object encapsulation (OOP style)

We can define an object that holds the variables envisioned as global and define functions for that object. (Using SubValues.)

For example:

ClearAll[PlotObject]
PlotObject[id_]["Line"[l_]] :=
  Module[{q, V, W, obj = PlotObject[id]},
   V = obj["W0"].straight[l] + obj["V0"];
   q = {RGBColor[0.5, 0.5, 0.5], CapForm -> "Butt", Tube[{obj["V0"], V}, obj["size"]]};
   W = obj["W0"].obj["mstraight"];
   obj["W0"] = W;
   obj["V0"] = V;
   q
  ];

Here we create the object and set parameters:

ClearAll[obj1]
obj1 = PlotObject[Unique[]];
obj1["V0"] = 12;
obj1["W0"] = RandomReal[{0, 1}, {3, 3}];
obj1["size"] = 200;
obj1["mstraight"] = {0, 0, 1};

And here is a function call:

obj1["Line"[34]]

(* {RGBColor[0.5, 0.5, 0.5], CapForm -> "Butt", 
 Tube[{12, 12 + {{0.337577, 0.582427, 0.344005}, {0.333857, 0.879125, 
   0.867341}, {0.345823, 0.873797, 0.344179}}.straight[34]}, 200]} *)

For more details how to use this OOP style see this blog post "Object-Oriented Design Patterns in Mathematica" and the references in it.

Other OOP styles in Mathematica are referenced in "Which Object-oriented paradigm approach to use in Mathematica?".


I like Anton Antonov's answer and I would like to offer this utility function in support of his Association method.

Attributes[aModule] = HoldRest;

aModule[asc_Association, body_] :=
  Set @@@ Hold @@ KeyValueMap[Hold, asc] /. _[set__] :> Module[{set}, body]

Now one can easily write:

foo = <|a -> 1, b -> 2|>;

aModule[foo, a = 2 b; a]
4
  • Global values of a and b are unaffected, as is foo.

  • The Keys must be Symbols. If people desire I can post a more verbose version of this code designed to handle arbitrary Key names.

This function is also useful for handling options, if passed as an Association or converted to such.

Likely related, though in 10.1.0 I do not have this functionality:

  • What's the purpose of the GeneralUtilities`UnpackOptions?

Generalization

When I wrote "arbitrary" above I was merely thinking of automatic conversion of String keynames to Symbols. At the same time it would be nice to handle delayed rules in the Association. That could be done something like this:

Attributes[aModule] = HoldRest;

aModule[asc_Association, body_] :=
  Replace[
    Join @@ Cases[Hold @@ Normal @@ {asc},
      h_[L : _Symbol | _String, R_] :>
        With[{sy = Quiet @ ToHeldExpression @ ToString @ L},
          Hold[h[sy, R]] /; MatchQ[sy, Hold[_Symbol]]
        ]
     ],
    {(_[a_] -> b_) :> (a = b), (_[a_] :> b_) :> (a := b)},
    {1}
  ] /. _[sets__] :> Module[{sets}, body]

We can now also do this:

aModule[<|x :> Print[1]|>, Do[x, {3}]]  (* 1 printed three times *)

aModule[<|"a" -> 1, "b" -> 2, "c+d" -> 3|>, a = 2 b; a]
4
  • "c+d" -> 3 is ignored as its Key cannot be directly converted into a Symbol.
  • Unlike the simple code at the start of this answer this function will not work with something like a =.; foo = <|a -> 1|>; a = 2; aModule[foo, a += 3; a] due to my use of Normal. If that edge case should be handled it can be, but again the code will become longer.

If we really want arbitrary key names that is a rather different problem. Here is a first attempt at approximating this idea.

Attributes[aModule] = HoldRest;

aModule[asc_Association, body_] :=
  Module[{f},
    Replace[Hold @@ Normal @@ {asc}, {
      (L_ -> R_) :> (f[L]  = R),
      (L_ :> R_) :> (f[L] := R)
     }, {1}] // ReleaseHold;
    Unevaluated[body] /. x : Alternatives @@ Keys[asc] :> f[x]
  ]

This allows things such as:

aModule[<|"c+d" -> 1, {12} -> 2|>, "c+d" = 2 {12}; "c+d"]
4

As written this has the disadvantage of using indexed objects in place of Symbols which means that among other things this won't work:

aModule[<|a -> {1, 2, 3}|>, a[[2]] = 5; a]

Set::setps: f$2174[a] in the part assignment is not a symbol. >>

With a yet more verbose definition Unique Symbols could be created for each replacement but I am unsure of the merit of general replacement like this to begin with; is using a Key name like {12} and treating appearances of it as a Symbol actually useful or just confusing? Is there a less confusing way to approach this that would usefully extend aModule?

Tags:

Programming