I believe they are referring to the @immutable macro described earlier, but you’re right, it’s just a way to wrap things in ReadOnlyArray or possibly ReadOnlyStruct, it’s not even close to the C++ const you’re proposing.
The macro would just wrap the mutable instance in an immutable struct instance that forwards field access to the mutable instance. However, mutating setproperty! is intentionally sabotaged. Substituting that immutable instance as an input will accomplish some enforcement of immutability, specifically throwing errors at x.a = value.
Julia very specifically tries to be more expressive rather than more restrictive.
About the most expressive you can be is to have macros, because then if you want some language feature, you can mostly just build it yourself. If many people want this language feature, then it becomes maybe part of a standard library. This is more or less the Julian way.
It depends much on what you mean by “works”. And I still don’t understand the behavior you want, whether it’s a runtime behavior or a parse time behavior or a compile time behavior… these are all different things in Julia.
It still seems unlikely that Julia, the language and default toolchain, will be able to effectively meet your request at the moment. Essentially the issue is that we do not have a distinct code analysis step in the default workflow.
What may be possible, if you are willing to add an additonal step, would be for a code analysis tool to perform this check given sufficient annotation.
The leading code analysis tool is JET.jl.
Code analysis tools should become easier to build as well with JuliaSyntax.jl:
This could be integrated with testing and continuous integration. While this means that the workflow may become similar to that of statically compiled langauges, perhaps this is not something you would not mind?
It’s not impossible to write a macro that does very specific code analysis though. Like if a group has some coding conventions they want to enforce and can describe them as syntax requirements (like don’t put certain vars on the left side of assignment operators etc) then you could write a macro
Something like that. Normally after agreeing over an interface in implementation the compiler should not compile if something not complies with the interface like it does in other languages. I would preffer the reversed behaviour of C/C++ everything is const until explicit specified otherwise feels more natural and short.
Or something like this:
function foo(a!, b) #or foo(mut a, b), whatever
a[1] = 1
end
I think the Julian way to handle this will be a macro. Note that julia compiles the code at runtime when you first call it. But I doubt you’re looking for a “runtime” error in the middle of testing your code, you’re probably hoping for something that errors out as soon as you’re done inputting the code, and that’s parse time.
I’m not sure exactly what such macros would look like, but they would really vary from one developer group to another. Another way to go would be the Jet route. Though I’m not familiar with Jet, I probably need to get familiar with it.
I’m guessing you want something like:
@enforce_rules function foo!(a,b!)
...
a[1] = 2
end
The @enforce_rules macro would walk through the code and see if there were any instances of you doing things you shouldn’t do according to whatever your own rules are for what you want to enforce.
If catching syntactic sugar like a[1] = 2 or a.x = 3 was all it needed, then this would be a far simpler task that could be done at parse-time. But the big problem is most mutating methods are just function calls. They do eventually dispatch down to a handful of fundamentals like setindex!, setproperty!, push!, etc., but method dispatch is something that is only figured out at compile-time, long after parse-time. And sometimes, the compiler can’t even find the methods at compile-time, it needs runtime types to do runtime dispatch.
Yes, agreed. But I think the OP is really looking for something simpler than “we have a formal proof that Julia will never mutate this thing”.
A macro could in fact lower the code and then go through and make sure you don’t call setindex! or setproperty! on any variable that appears in the argument list of the function which doesn’t end in ! for example. I think that is the kind of thing OP wants?
Note, I’m not entirely clear on how a macro would force the code to be lowered. I’m guessing there is some lower_code(expr) function in julia but I don’t know details. In this case, perhaps it could simply replace = with setindex! or setproperty! calls itself.
One thing that is often not obvious to those comparing is that in C++ it is possible for the compiler to know everything about a function or about all “methods” from a template all at once.
In Julia, at runtime someone can redefine a method or define a new method that operates on new types. At any moment the meaning of foo(a,b) can change. It is approximately like you are compiling a program in C++ which is free to write out files in C++, call g++ on them, link them into the program, and then edit all the jumps in existing code to jump to the new definition. And, furthermore this can all be triggered by user input…
I don’t know how we should be done best. But as strong enforcement as possible and simple as possible will be recomended.
Let’s take the swap case. The simplest way of solving it and will still work in 1.x too is:
#instead of:
swap!(a, b)...
#let's postfix the mutable variables with an '!'
swap!(a!, b!)...
#I like with the
swap(a!, b!)...
# but is not named using current Julia conv to have the '!' at end.
In future this variables can be better checked and enforced as the time passes.
I would dare to say that Julia is very similar with C++ at compile time. All this post was about enforcing and clarifying (im)mutability of fields in each function. So is relevant and shound check and behave similarly at the moment of compilation as I pointed before trying to compile an offending C++ example.
Reminder:
void f(const vector<double> &v) { v[0] = 2; }
error C3892: 'r': you cannot assign to a variable that is const
But when is “compile time”? You’re aware that it’s the moment during the running of the code that the function is first called right? And that, any arbitrary amount of time could have passed since the initial start-up of your program. Like it could be 3 years after starting your program that the function is finally compiled.
Is pretty much the same in both languages. Julia model is just the C++ compile time polymorphism that is not implemented safe enough yet and this is what I want to discuss and improve.
We call it in Julia multiple dispatching and are proud because de C++ don’t bother with this.
Right, which is when the function is first called. So for example
function foo(a,b)
bar(a) + baz(b)
end
as long as bar and baz don’t mutate anything, this is a non-mutating function…
So we start our program running… 3 years later it’s running on a Mars rover and someone sends some sort of command which causes us to call foo(a,b) on two values which have never been seen before, perhaps they’re specialized structs, and perhaps we’ve sent the mars rover a file and it “include(“thefile.jl”)” which defined those structs, and also defined bar(::ThatStruct) and it did so in such a way that now bar is mutating things…
How would C++ handle that? (hint: it wouldn’t)
Note also that Julia multiple dispatch is NOT the same as C++ templates. Because C++ templates all have the same code… it’s ONE template specialized to multiple types. In Julia every method can do something entirely different. 100% different code.
function quux(a::Int64)
a+1
end
function quux(a::Float64)
sqrt(a+1e6)
end
C++ is a serious and mature language. We can complain about some attrocities that came from C era but now at C++21 is a very serious language and CT polymorphism is solved for a long time. Let’s dont open this topic here.
Very simple in C++ f(const T&) and f(T&) are too different functions this should be in Julia too. If at a given moment a function calls f(const T) a code that requires f(T) needs to see f(T) implementation.
In your case with quux example you don’t modify the a variable.
When do you get the error: In Julia it will be 3 years after you launch the Mars Rover
How do we distinguish between f(::MyType) and something let’s call it f(::Const{MyType})
Is 1) satisfactory to you? 3 years after launching your rover you finally discover that your code did something stupid under some circumstances?
And part (2) has already been shown further up thread. Julia can mostly handle this, you just create “wrapper types” which prevent setindex! and setproperty! from working right, but it’s up to the caller to call your “separate function” by handing it things of the right type, namely constant structures.
I think you are missing my point. C++ just does not handle the case where “3 years after launching the rover we send the rover a code file that defines some new types and a bunch of new functions that operate on those types but with the same names as other functions”. When you run g++ it sees the entire code all at once and links everything, and then it’s all static. Julia by design does not have this “all at once ahead of time static compiled” design.
But the point of that example was that quux does totally different code depending on what the type of the argument was… In C++ templates the code is the same it’s just specialized during compile time to know exact offsets of certain struct values and etc.