How to make Julia functions pure mathematical functions?

Hi Julia community,

Problem:

I would like to define a function which has only access to the arguments passed to that function.

The problem is illustrated in the following minimal working example.
I would like that the function tmp() throws an UndefVarError: x.

x = 17

function tmp()
  x + 20
  println(x)
end

# will print 37
tmp()

Background:

I write a large script with intermediate results, which are used later on by different functions.
Each relevant intermediate result is saved as a constant variable.

const var1 = f1(a, b, c)
const var2 = f2(var1, b)
const var3 = f3(var1, var2)
etc...

Do you have suggestions for a better practice?

1 Like

I’m not sure if I can answer your question well (maybe somebody else will come with a perfect solution :wink: ) Instead of relying on a special package, which detects the use of globals (e.g. JunoLab/Traceur.jl (github.com) ) you should rather train yourself to make sure you write functions which only uses local variables and never globals.

Note that the scoping rules are such that the same variable name (alias) can be used in a local setting such that a global with the same name is ignored, e.g.

x = 17
function tmp()
    x = rand( 1:10 )
    x += 20
    println("Local value of x: ", x)
end

tmp()
println("Global value of x: ", x)

The key point is, that inside a function you should always create the variables you want to use, e.g. writing x + 20 without having created the variable x in the local context is never a good idea.


(Note that the situation is different if you have nestled functions, then something called captures happen.)


Now, in your background example, I am not sure why you define these constants in the first place.
Maybe you could provide a bit more details on what you want to do.

I usually do this:

  1. usually, all functions I define will not depend on global variables at all.
  2. it’s ok if the same variable name is used globally and inside a function, those are two separate things anyway!
  3. if I need to pass a lot of variables, I like to use NamedTuples or keyword arguments, which are convenient and fast to move a collection of variables without needing to worry about a special ordering or structure.
f1(a, b, c) = a+b
f2(a, b, c; d = f1(a,b,c)) = c+d
f3(x; p) = x + p.a + p.d


a =  10
b = 11
c = 12
d = f1(a,b,c)
h = f2(a,b,c; d=d)

p = (;a, b, c, d)   
f3(20; p)   # note that the `;` is used here to separate positional arguments and keyword arguments
2 Likes

Why would you like to have the error message? For a Mathematical function, as the title indicates, I would expect, that all variables necessary are passed into the function, i.e. your function would look like

function tmp(x)
    x + 20 # what is happening to the result here anyways?
   print(x) # should this print x or the result from before?
end
# or
function tmp!(x)
    x += 20 # since we modify x we use the !
    print(x)
   return x
end

If you are unsure which variables are computed already and want to react with reasonable error messages or default ways, maybe you could always pass a Dict and check whether certain entries exist?
So maybe along the lines of

d = f(a, b, c) # returns a dictionary
f2!(d, b) # could check that the result of f1 is saved in d[:var] or something
f3!(d)
...

That way every function in your script could “check” that all previously computed values it needs are there?

Not sure whether that is what you intended, maybe just what I read from your request.

@SteffenPL thank you very much for your answer!

I elaborate a little bit more on that:
I do a data analysis, i.e. load the raw data, rearrange data, calculate new things from the data and combine results to make figures and so on.
For example the rearranged data is input to different functions, so I save it once as a constant variable (that it has not to be calculated again and again).

@kellertuer thank your very much for your answer!

Because I find it irritating and prone to errors, when global variables are known to a function without explicitly passing them as arguments to that function.

I would like to eliminate this potential error source.

The easiest solution was proposed by @SteffenPL – never access them, that is make sure, you always create a local variable before you use them (or add all that you do expect to be passed to the variables/kwargs), maybe that way you can easier avoid it? In your example before using x in x+20 make sure to declare the variable in local scope.

edit: I think the dictionary I posed might also be a little inefficient, so maybe Steffens approach is indeed nicer then.

PS: Oh I just saw that at the top – Welcome to the Julia community (forum)!

I’m guessing the reason you want this is that you are using the same identifiers for your global constants as for the local variables inside your function? A simple solution would be to just name the globals differently…

A hacky way to prevent accidental use of globals would be to create an empty module, and evaluate your function definition there. That way, you can only access your globals by prefixing them with Main.

x = 3
function f end

module Empty
function Main.f()
   x+1
end
end # module

f() # UndefVarError: x not defined

I would not use leave the Empty module in the final code, but it might be an easy way to check for accidental use of globals while developing…

1 Like

Hey, I asked a similar question here once upon a time more from a conceptual point of view! You may find the discussion here interesting: Can Programming in Julia Be "Pure"? - #13 by dlakelan

Welcome to the Julia Community too! :wave:

1 Like

A “better practice” approach is to put functions inside a package, which automatically puts them in a different module (i.e. name space) from your script.

The script can be developed together with the package, and even live in the same git repository, but because of the different name-spaces it is not possible to use globals from the script within the package by accident.

4 Likes

Yeah very nice and definitely related discussion!

@Per

That’s true, very good guess!

That sounds good!
Does this also work If I organise my functions within modules (not necessarily creating a package), load them into the script and evaluate them within the script (not within the module)?

Thank’s to all for the warm welcome to the Julia community :blush:

2 Likes

But this doesn’t modify x. It creates a new local variable binding, x.

julia> x = 5
5
julia> tmp!(x);
25
julia> x
5

You are right, I was thinking too much in Arrays :wink:

Yes, this would also work, however creating a package involves almost no extra work, and it also has other advantages such as keeping track of your dependencies.

1 Like