Creating a function that does not import outer scope variables

Julia allows functions to see variables outside of their scope, something like

y=2
function f(x)
   x+y
end

I agree this can often be useful, but for me this is generally a source of bugs. A typical error that I do is

y=2
function f(x)
   y\hat=4
   x+y # I wrote y but I had intended my code to use y\hat   
end

This kind of error is terribly hard to catch, specially when y and y\hat are similar, which they tend to be. Is there a way to define a function such that variables can only be used if they are explicitely passed as arguments (or if they are const)? I tried to use let block, but in that case the defined function stayed in that local scope and I could not use it.

1 Like

Perhaps you could enclose the function in a baremodule and then import that method into your desired module.

https://docs.julialang.org/en/v1/base/base/#baremodule

julia> x = 5
5

julia> baremodule Foo
           f() = x
       end
Main.Foo

julia> import .Foo: f

julia> f()
ERROR: UndefVarError: x not defined

A standard module does the same, doesn’t it?

julia> x = 1
1

julia> module A
           f() = x
       end
Main.A

julia> A.f()
ERROR: UndefVarError: x not defined
Stacktrace:
 [1] f()
   @ Main.A ./REPL[2]:2
 [2] top-level scope
   @ REPL[3]:1

Maybe the advice here is the basic one: don’t use global variables (in this case, within the module being developed), or at least keep them to a minimum, to avoid this kind of scoping confusions.

Yes, for x. I was thinking he might want as complete namespace isolation as possible. In a regular module you still have imports from Base. For example, π.

julia> module Foo
           f() = π
       end
Main.Foo

julia> Foo.f()
Ď€ = 3.1415926535897...

julia> baremodule Bar
           f() = π
       end
Main.Bar

julia> Bar.f()
ERROR: UndefVarError: π not defined
3 Likes

I think that a (bare)module is more restrictive than what I want, because I want to be able to call other functions from inside the function I defined. I could pass as a parameter to the function in the module other functions, but then it becomes a little bit too much I think.

You just need to import exactly what you want to use. Otherwise, the answer is simply do not use globals variables at all. Use a let block to create a local scope instead of assigning to a global variable.

let y=2
    # do something with y
    println(y)
end
function f(x)
    y\hat=4
    x+y # I wrote y but I had intended my code to use y\hat   
end

The info that f uses a global can be accessed like:

julia> y = 2
2

julia> function f(x)
           x+y
       end
f (generic function with 1 method)

julia> code_typed(f, (Any,))[1]
CodeInfo(
1 ─ %1 = Main.y::Any
│   %2 = (x + %1)::Any
└──      return %2
) => Any

so one option might be to write a macro like @noglobals function f(x) ... which defines the function, then calls code_typed on it with some valid set of argument types and crawls the codeinfo object to determine a global was used then errors. Its tricky though because if you call other functions like x + sin(y) you’ll get a Main.sin “global” in there so you’d have to differentiate that, assuming you want to allow it.

Alternatively, the @noglobals macro could identify all the variables and add unassigned local statements for them, so if a variable is never assigned, the method call would throw an UndefVarError instead of accessing a global variable. It wouldn’t require providing argument types to narrow down the call for inference, but it might be tricky making sure the local statements don’t change the method’s behavior. For example, you wouldn’t want to put local x inside a for-loop that accesses an outer local x just because the macro found an x in the block.

1 Like