I would like to understand Julia’s rationale for not including a language-level privacy feature: a way to truly protect the internal state from outside access.
I’m aware that we can use closures to emulate encapsulation, but that’s cumbersome, and it’s not really the same as a built-in mechanism. I’ve often heard the “We’re All Adults Here” mindset mentioned, but I find that unconvincing; even experienced developers introduce critical bugs—memory-safety issues in C/C++ being just one high-profile example. Of course, Julia’s garbage collection helps prevent memory corruption, but direct messing with internal structs can still lead to logical or invariant-breaking bugs.
So, is there a deeper reason why Julia can’t (or shouldn’t) offer an opt-in privacy model?
I’d personally find it fair if those who want stricter encapsulation “pay” with potential performance or debugging complexity. But I’d love to hear the community’s thoughts on whether this is feasible or if it conflicts too deeply with Julia’s design goals around metaprogramming and introspection.
I found this weird, too, at the beginning. Now I believe:
the Julia status quo is preferable to what the stricter, “object-oriented”, languages have
the status quo will improve further
The benefits exist for interactive usage, e.g., in exploration in the REPL, also testing, in some cases. This is huge and I don’t think any experienced user is willing to give it up.
The drawback is packages being able to trespass on the internals of the Julia implementation, or the internals of other packages, leading to breakage on a new release of such an intruded-on dependency. IMO the problem isn’t being able to poke at internals of dependencies on its own, the problem is being able to do that while failing to pin the version of that dependency properly in Project.toml.
So I expect this will be solved in a best-of-both worlds manner; now that we have public and Base.ispublic, the required validation can happen programmatically. It’s possible to:
have a check disallowing the registration of package release
It’s not only structs, but also functions/methods. Nothing prevents you from making e.g. Int an iterator to iterate through the numbers below (if one believes in the von Neumann definition of the natural numbers):
function Base.iterate(n::Int, state=nothing)
isnothing(state) && return n<=0 ? nothing : (0, (1,n))
next, lim = state
next ≥ lim && return nothing
return (next, (next+1, lim))
end
Base.IteratorSize(::Type{Int}) = Base.HasLength()
Base.length(i::Int) = i
julia> collect(5)
5-element Vector{Int64}:
0
1
2
3
4
However, a lot of things will be messed up by this fancy trick, since they may rely on the default behaviour, that collect(5) returns a 0-dimensional Array containing 5.
The same goes for every method in every package. Anyone can replace any method. There is work underway to make it possible to “freeze” functions so that methods can’t be overwritten or added.
It also lets you extend behavior without waiting for the public API to allow it, and it’s often not a reasonable expectation for the public API to irreversibly support something that you may not want for long. This is how third party libraries can pull off static type inference or compiler plugins in a typical Julia process. But as you said, this is exceptional; for the most part we are heavily incentivized to not rely on things that can break on minor releases, hence the strong encouragement of documentation, leaning toward documenting methods instead of fields, and more recently a formal specifier of public global names (so not public fields, or rather properties). The more common usage of internals is interactive reflection without needing to write extra API from a reflection library or resort to undefined behavior. Historically, the “who needs access modifiers, we’re all adults” philosophy is standard in popular, interactive-first, dynamically typed languages preceding Julia e.g. Smalltalk, MATLAB, Python, R. Julia also cannot share the access modification of OOP languages that have class encapsulation; we can’t nest private types in other types. If we couldn’t access types with non-public global names, they’d be useless.
While it’s not true access modification, you can also make it harder for people to access fields by overloading getproperty and, for mutables, setproperty!. That forces them to resort to getfield/setfield! instead of using dot syntax.
I don’t think you can “truly” protect internal state from outside access in a compiled language. You can always convert to a Ptr and access/modify data manually. So privacy is a question of programming convenience, not of actual safety.
So with that in mind: the benefits of a strict enforcement at compile time versus just telling users “don’t do this” is marginal.
In my own code I violate the “don’t do this” all the time: sometimes I need an internal function and I just don’t want to take the time to copy-and-paste it, or I worry that any improvements in future versions will be missed out. This does make my packages somewhat brittle, which is not great, but CI in Julia is very effective at catching bugs that result from this.
I am having trouble right now with wrapping a C++ library where too much of attributes are made private. As you have said, it’s possible to circumvent that by taking raw pointers etc. and it would be much more brittle than in Julia, I think. So, I’d say the contrary is true: if you end up needing to break the rules, with C++ approach it’ll be inherently brittle (dependent on platform, compiler conventions etc.) while with “don’t do this” approach it may be done reasonably robust.
Not in practice, at least not in general. All fields are declared as public properties by default, and it’s actually kinda rare to define a propertynamesmethod for one’s type, so hasproperty will usually answer true even if the property isn’t supposed to be public