Skip to main content

programming - Functions with changeable global variables


I'm not sure is the topic title correct, but I mean such a case.



I have several functions and "changeable" global variables, e.g.:


f1[x_]:=Module[{q}, q=expr1[x, V0]; V0=expr2[V0]; q];

This means that function f1 depends on x explicitly and on global V0 implicitly. Inside this function we compute some expression expr1 and return its result q as a result of functioin f1. V0 has some initial value before f1 run, and this value is changed inside f1 ( as result of expr2). This changed value of V0 is now initial value for some other function f2, which also may change V0. These functions f1, f2 run inside Which construction: if an element of some list has specific property this triggers one of the f1, f2, ... functions, after each fi run the value of V0 is changed and this new value is initial value for next fi.


The question is: how to correctly organize all this? Where to initiate V0: at the very beginning of Notebook (as individual Input), or inside Which construction (Which is enclosed in Module)?


Thanks.


EDIT


As I was asked in comments for details of functions and variables, there are examples.


One of the functions draws a line:


line[l_]:=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; W0=W; V0=V; q];


Other functioins look similar. So, line[l] takes length of straight as input, and also depends on "global" variables V0, W0 -- initial coordinate and direction. V0 is vector, initially defined as {0,0,0} at the very beginning of my nb file. W0 is rotation matrix depending on 3 angles, this may change after function drawing arc, and initially W0 = IdentityMatrix[3]. straight[l_] = {0,0,l} and because line doesn't change direction, mstraight = IdentityMatrix[3], size is global constant defining size of all the straights. Hence after this function line run I have Tube object (which can be drawn later together with other objects) and changed V0, W0 for input to next function as new initial coordinates and direction. Now I have these variables as globals initiated at the beginning of nb, so that every such function knows their instant changed values and may change as well.


I ask, if this approach is incorrect, please explain why and how to change it to be right.


EDIT 2


Many thanks to Anton Antonov for his versatile answer. My present code is as follows:


Module[{},v0={0,0,0}; W0=IdentityMatrix[3]; size=0.2;
(* here go other initials and constants*)
graphics=Reap[(Which[list[[#]]==somevalue1,Sow[line[...]],
list[[#]]==somevalue2,Sow[arc[...]] (* and so on*)]&)/@ Range[Length@list]][[1]];]


With Anton's approach I have to change Sow construction to compound expression like this:


(Sow[line[...][[1]]]; {V0, W0}=line[...][[2]];)

Well, for me these additions complicate and lengthen code. I'm not programmer, just beginner in using MMA and WL, and for me my code looks more simple and transparent to control and understand, all needed changes in V0, W0 are done automatically beacause these variables are global.


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. I really don't understand the advantages of approach like proposed by Anton.



Answer






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:



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).




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},
V = PlotObject[id]["W0"].straight[l] + PlotObject[id]["V0"];
q = {RGBColor[0.5, 0.5, 0.5], CapForm -> "Butt",
Tube[{PlotObject[id]["V0"], V}, PlotObject[id]["size"]]};

W = PlotObject[id]["W0"].PlotObject[id]["mstraight"];
PlotObject[id]["W0"] = W;
PlotObject[id]["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?".


Comments

Popular posts from this blog

plotting - Plot 4D data with color as 4th dimension

I have a list of 4D data (x position, y position, amplitude, wavelength). I want to plot x, y, and amplitude on a 3D plot and have the color of the points correspond to the wavelength. I have seen many examples using functions to define color but my wavelength cannot be expressed by an analytic function. Is there a simple way to do this? Answer Here a another possible way to visualize 4D data: data = Flatten[Table[{x, y, x^2 + y^2, Sin[x - y]}, {x, -Pi, Pi,Pi/10}, {y,-Pi,Pi, Pi/10}], 1]; You can use the function Point along with VertexColors . Now the points are places using the first three elements and the color is determined by the fourth. In this case I used Hue, but you can use whatever you prefer. Graphics3D[ Point[data[[All, 1 ;; 3]], VertexColors -> Hue /@ data[[All, 4]]], Axes -> True, BoxRatios -> {1, 1, 1/GoldenRatio}]

plotting - Filling between two spheres in SphericalPlot3D

Manipulate[ SphericalPlot3D[{1, 2 - n}, {θ, 0, Pi}, {ϕ, 0, 1.5 Pi}, Mesh -> None, PlotPoints -> 15, PlotRange -> {-2.2, 2.2}], {n, 0, 1}] I cant' seem to be able to make a filling between two spheres. I've already tried the obvious Filling -> {1 -> {2}} but Mathematica doesn't seem to like that option. Is there any easy way around this or ... Answer There is no built-in filling in SphericalPlot3D . One option is to use ParametricPlot3D to draw the surfaces between the two shells: Manipulate[ Show[SphericalPlot3D[{1, 2 - n}, {θ, 0, Pi}, {ϕ, 0, 1.5 Pi}, PlotPoints -> 15, PlotRange -> {-2.2, 2.2}], ParametricPlot3D[{ r {Sin[t] Cos[1.5 Pi], Sin[t] Sin[1.5 Pi], Cos[t]}, r {Sin[t] Cos[0 Pi], Sin[t] Sin[0 Pi], Cos[t]}}, {r, 1, 2 - n}, {t, 0, Pi}, PlotStyle -> Yellow, Mesh -> {2, 15}]], {n, 0, 1}]

plotting - Mathematica: 3D plot based on combined 2D graphs

I have several sigmoidal fits to 3 different datasets, with mean fit predictions plus the 95% confidence limits (not symmetrical around the mean) and the actual data. I would now like to show these different 2D plots projected in 3D as in but then using proper perspective. In the link here they give some solutions to combine the plots using isometric perspective, but I would like to use proper 3 point perspective. Any thoughts? Also any way to show the mean points per time point for each series plus or minus the standard error on the mean would be cool too, either using points+vertical bars, or using spheres plus tubes. Below are some test data and the fit function I am using. Note that I am working on a logit(proportion) scale and that the final vertical scale is Log10(percentage). (* some test data *) data = Table[Null, {i, 4}]; data[[1]] = {{1, -5.8}, {2, -5.4}, {3, -0.8}, {4, -0.2}, {5, 4.6}, {1, -6.4}, {2, -5.6}, {3, -0.7}, {4, 0.04}, {5, 1.0}, {1, -6.8}, {2, -4.7}, {3, -1....