One common idiom here is not through a module, but through let scopes:
julia> let
global get_secret, set_secret!
secret = "I'm hidden"
get_secret() = secret
set_secret!(v) = (secret = v)
end
set_secret! (generic function with 1 method)
julia> get_secret()
"I'm hidden"
julia> set_secret!("I'm still hidden")
"I'm still hidden"
julia> secret
ERROR: UndefVarError: `secret` not defined
If you use a Julia module here, you’ll be able to directly access MyModule.secret.
For a long time I also thought that this would work, until I saw post where someone showed that the secret can be accessed in the following undocumented way:
I mean, there will always exist, somewhere in your program, the string "I'm hidden !" along with a way to access and modify that binding. If someone can run arbitrary code within that program, they will be able to reproduce whatever Julia itself does, either through your “official” functions or whatever internals (documented or not) Julia itself uses to effectuate the change. The only question is how hard can you make it.
This mean nothing can totally be private in Julia.
But this pattern is still pretty effective, since it can be used in a regular module to keep some data hidden, I doubt someone will put that much effort to access a variable
setSecret doesn’t do anything here, it just creates a new local secret and throws it away immediately. global secret = s would fail in v1.11- because it’s const. v1.12+ allows const redefinitions (with reasonable limits on its influence), but not like that.
But it’s worth pointing out that the global get_secret, set_secret! methods in a let block aren’t treated as closures and don’t get the named fields that a closure would; the local secret is boxed separately and the method just references it namelessly. The same effect can be flipped with local in begin:
julia> begin # in global scope
local secret = "I'm hidden"
get_secret() = secret
# borrows outer local
set_secret!(v) = (secret = v)
end;
julia> @code_typed set_secret!(3)
CodeInfo(
1 ─ Core.setfield!(Core.Box("I'm hidden"), :contents, v)::Int64
└── return v
) => Int64
julia> set_secret!.secret
ERROR: type #set_secret! has no field secret
I actually don’t know of a way to access that box, I imagine it’ll take some pointer magic instead of a few base functions.
Some variant of that will always be true. Julia is not a sandboxed language. There are no security boundaries within julia: That is the operating system’s job, leveraging hardware capabilities.
The same is true in modern java: security manager / applets are dead, because they were a neverending wellspring of vulns.
The same is mostly true in node.js.
The only exception is javascript (and webassembly) in web-browsers. Browsers are effectively operating systems, and it is essential that they can run malicious code with bounded permissions (i.e. shady websites’ JS). And this is a great feat of engineering, effectively the only software-defined sandbox that works! (you could argue about ebpf, but I’m not running ebpf code from shady websites in my kernel)
So you should absolutely forget about using language features for “secrets”. The entire concept and question is misguided.
Instead, properly document that this is an internal thing that your module’s users shouldn’t access, on pain of breakage in the next update.
You as a package author are not in control – ideally the person with physical access to the machine running the code is in control, and your job is to make their lives easier. They can access the field. If it’s stupid to access the field, warn them, but don’t make futile attempts to prevent it. They can anyways pull the plug, use freeze-spray on the RAM and then read the field, shitting all over the tower of abstractions in your mind.