Overwriting functions in another module

Redefining some module’s values/functions directly is not allowed:

module testm

s = 1

function f(x)
    x/2
end

end

testm.s = 3

ERROR: cannot assign variables in other modules

g = x -> x/10

testm.f = g

ERROR: cannot assign variables in other modules

However, this works:
testm.f(x) = g(x)

testm.f(1)

0.1

What is the reasoning behind such behavior? Thanks!

I filed an issue about it https://github.com/JuliaLang/julia/issues/23295. I find globals occasionally convenient.

There’s two work-arounds, either you use

const my_flag = Ref(false)

like Revise.jl does or you use @eval ThatModule some_global = 10.

Regarding testm.f = g, there’s a technical reason for not allowing assigning to a function. Julia has multiple dispatch, and it would make the compiler’s job much harder/impossible if it gave you the option to redefine functions like that.

You can also use setter functions:

julia> module A
       glob = 1
       change_glob(v) = global glob = v
       end

julia> A.glob
1

julia> A.change_glob(4)
4

julia> A.glob
4

1 Like

Thanks! I’m still trying to understand the reason for how testm.f(x) = g(x) actually work as “redefine test.f” though.

You’re not allowed to change variables in other modules, but you are allowed to add/modify methods for functions defined in other modules.

see Methods · The Julia Language

3 Likes

Got it. Thanks!

You can’t write testm.f(x) = g(x) inside of a function, to redefine it at runtime. This restriction allows the compiler to assume that, once you’re pressed Enter at the REPL, f(x) = g(x) will be true until the computation finishes (barring some technicality with @eval and the world age).

Globals can be changed at runtime anywhere, so the compiler can’t assume anything about them, and their performance is terrible.

That’s why functions and globals are separate concepts in Julia, whereas in Python function definitions are (basically) implemented as global variables.

2 Likes

I am not sure what the original design reason was, but I like it this way. I consider globals as internal to a module, mostly an implementation detail, which should only be exposed via accessor functions outside the module.

Eg suppose I have a package for managing some templates, and have a setup like

module TemplateManager

export default_template_dir

DEFAULT_TEMPLATE_DIR = expanduser("~/templates")

default_template_dir() = DEFAULT_TEMPLATE_DIR

end

Later on I can decide to make the global a Ref, fixing the type, or allow the user to set it via an environment variable and just provide a fallback, eg

function default_template_dir()
    get(ENV, "DEFAULT_TEMPLATE_DIR", DEFAULT_TEMPLATE_DIR)
end

without changing the API. Similarly, I can make a setter function

function default_template_dir!(path)
    isdir(path) || @warn "setting non-existent path for templates"
    DEFAULT_TEMPLATE_DIR = path
end

that validates the input.

1 Like

I’ve been experimenting a lot with similar issues related to dependency injection, and my current preferred approach is to eval within the scope of the targeted module. So if you’re looking for a way to actually do what you tried, this works:

julia> Core.eval(testm, :(s = 3))
3

julia> testm.s
3

Or TestModule.eval(:(s = 3)) (note that A.@eval s=3 does not work).

Nice, shorter.

Or if you want to write more complex expressions:

@eval TestModule begin
    s = 3
end
1 Like