Typeconst proposal

Great discussion, which added a lot of nuances and explicit advice based on use cases.

The difficulty of expressing this so clearly might suggest that the keyword const is a misnomer, though it helps the Julia compiler optimize for the types of globals so defined.

Two suggestions:

  1. Rename the keyword const to typeconst. It’s a weird non-word but it makes the purpose clear and is odd enough not to be confused with any notion of immutable.

  2. Introduce a new keyword immutable that can be used as a modifier for variable initialization. This is a new feature unrelated to type stability of globals (although it provides that, too). For example immutable: a = 5. We don’t need to say ::Int32 (or whatever) because type inference will handle that in this case. This would define a true immutable (variable binding and value) similar to what one can do in other languages. The purpose is to create an understandable (hopefully) alias for some important constant so that it can be used throughout source code.

Example:
immutable: scaling_factor = 1.37e-3

Later, in the code

     force = g * scaling_factor   # yes, this is bogus...

That’s clearer than putting the float literal in the code. The compiler would need to enforce this and raise errors on any attempt to set a new binding for scaling_factor.

Certainly neither is a high priority but could add some clarity for type constant globals and add a feature for folks (like me) who originally misused const as an immutable.

https://github.com/JuliaLang/julia/pull/43671#pullrequestreview-873964604 it looks like it will work in 1.8.

2 Likes

Almost.

Looks like a macro @typeconst and type annotations (with conversion) but no notion of truly immutable.

This is awesome! Once it’s merged I suppose we can finally remove the “feature” that allows rebinding constant variables :smiley:

@lewis: the typeconst name would not be appropriate, as explained here: Const variables - #13 by Henrique_Becker . It gives the wrong idea that it’s OK to change the value of a const integer for example.

3 Likes

But, bizarrely it lets you try even though a warning, error, or general bizarreness results.

I’ll point out that the PR that another poster referred to does propose @typeconst, so it’s not just me…

That makes immutable: a = 5 seem all the more appropriate. Type and value can’t change. Real clarity and a nice extra feature (sort of…). Though you can still shadow globals, which I suppose is ok for more freedom naming variables.

  1. Rename the keyword const to typeconst. […]

As @sijo pointed out, unfortunately I think you did not understand completely the discussion above. const makes the binding constant, this means you cannot ever attribute a new value to the variable, i.e., you cannot do var = anything anymore (even if anything is of the same type than the original value). The object itself may be mutable, i.e., it can be a Vector, for example, and push! will keep working on it, so the contents of the Vector may change, but the object itself (and in other languages that expose this, the memory address) will always the same. The fact it cannot change type is just an immediate consequence of the fact you cannot change the object that is stored in the variable anymore, but it is a consequence, not the direct effect or meaning of const.

  1. Introduce a new keyword immutable that can be used as a modifier for variable initialization. […]

Julia already had an immutable keyword in the past (see the 0.3 documentation) but today we just do not use mutable before struct for the same effect, and even today there is a Base.isimmutable, so the meaning of immutability in the context of Julia is already well-defined. In your example, immutable: a = 5, the effect can already be achieved just with const because Ints are immutable, and with const also making the binding constant, then there is no way that global variable a can refer to anything distinct of 5 for the rest of the program.

This would define a true immutable (variable binding and value) similar to what one can do in other languages.

Which languages? The only one I know that really tried to go for immutability of value (in the sense you are defining it, which is a recursive immutability even for objects of types defined as mutable) is Ruby, and even them ended up never having a deep_freeze! function I think, see: Feature #2509: Recursive freezing? - Ruby master - Ruby Issue Tracking System and Feature #17145: Ractor-aware `Object#deep_freeze` - Ruby master - Ruby Issue Tracking System.

But, bizarrely it lets you try even though a warning, error, or general bizarreness results.

Which version of Julia are you using, mine is quite old (1.5.4) and there is an warning message in the REPL:

julia> const a = 5
5

julia> a = 10
WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.
10

And outside of the REPL I think the code will error but I have not tested this.

2 Likes

I’ve moved this typeconst proposal discussison into a new topic since the original topic was a “New to Juila” question and this is now quite far from that.

3 Likes

Why? It’s often useful during development to be allowed to either evaluate the definition of a constant multiple times with the same value, or reevaluate it with a different value of the same type. And since you’re usually reevaluating all the definitions that depend on it at the same time it’s even typical that everything works. Of course one should do that in normal usage, but that’s why there’s a fairly dire warning. Why does it bother people so much that this works with a warning? Would you be satisfied if there were an option to make the warning an error? I don’t get this insistence people have to not be allowed to do this. If you don’t like it, don’t do it. Is there something I’m missing here?

2 Likes

There are 3 reasons (in my opinion) to turn this into an error. The first is that disallowing mutation allows the compiler to inline const globals more often which can be a performance win. The second is that the main reason people use const globals in development currently is that you need globals to be const currently to be fast. If you can use typed globals in dev, you don’t need to be able to change the value as often. Also, it just seems like a hack that we have a const modifier that doesn’t guarantee that a variable is const.

3 Likes

We can also support the use-case of reevaluating all the definitions by allowing you to re-define a const global with the same value.

Ok, I’m glad to have actual reasons to discuss but I’m not convinced. I’ll respond to each point inline.

The first is that disallowing mutation allows the compiler to inline const globals more often which can be a performance win.

I think the compiler already has full license to do this. If it isn’t doing so because of the possibility for constant redefinition, that is news to me and I don’t think we should let that hold the compiler back—it should be inlining constants anywhere it thinks it would be beneficial. The warning for const redefinition is plenty dire and there’s no guarantee that anything that emits a warning will keep working in the future, so this can be changed at any time to be more aggressive.

The second is that the main reason people use const globals in development currently is that you need globals to be const currently to be fast. If you can use typed globals in dev, you don’t need to be able to change the value as often.

We can also support the use-case of reevaluating all the definitions by allowing you to re-define a const global with the same value.

Redefining a constant with the same value works great for immutables like const x = 123 but does not work for constant binding to mutable values like const A = [1, 2, 3]. When you re-evaluate that, it assigns a new, distinct array with the same contents. But it’s currently allowed with a warning because it’s handy and if you also reevaluate all the function definitions that depend on this, then everything actually works just fine. Having a genuinely constant global binding to a mutable data structure like this is quite common in code I write. You’re right that typed globals might help here, but as it is, I can just make the binding const like it actually is and still reevaluate the code just fine. Changing the definition to a non-const global is annoying and slows down any code that depends on the global. Changing it to a typed global is less of a performance hit but still an annoyance. Why should I have to do that when we have something that works in this case already?

Also, it just seems like a hack that we have a const modifier that doesn’t guarantee that a variable is const .

The guarantee is that if your program runs without warnings, then that global is never modified. In general, if you program prints warnings, you should be concerned that it’s not doing what it’s supposed to do. Working code should run without warnings. Are people just writing code that emits dire warnings and thinking “Great, that works!”?

I guess I would be fine with making reevaluation of const definitions an error by default and having an option to let me opt into allowing them, but man, that seems like an unnecessary option. But if that’s what it takes for people to stop complaining about being allowed to do something then :man_shrugging:

3 Likes

I’ll add something a bit higher level: Julia tries to be very friendly to REPL debugging in general and one of the main features of that is that you can copy-and-paste code from your program into the REPL and it should work. For me, that was the main reason to change the REPL behavior for assinging to an existing global binging from a “soft” local scope: not allowing that was a big impediment to REPL debugging of code from function bodies where this behavior was different. Yes, you can “just” cut-and-paste the code and then add global modifiers where necessary, but jeez is that annoying. I want the same code, without modification, to run the same way in the script and the REPL. Note that having to put a global modifier in a top-level loop in the script doesn’t present any problem because when I cut-and-paste that into the REPL it still works. It’s only cut-and-paste from a function body that presents a problem. The fix in 1.5 makes exactly that situation work.

This is a very similar issue. I don’t want to have to edit a const annotation in a file in order to evaluate it in the REPL, whether via include or literal cut-and-paste, both of which I do fairly commonly. And that includes multiple evaluations, because that’s what happens when you’re debugging. The current arrangement allows that quite nicely. Occasionally you decide a const global needs a different type, at which point you do need to restart the REPL, but that’s pretty uncommon, so not a big deal. Even that, I’d prefer if we went further and printed a dire warning and then allowed changing the type of the constant! Reassigning a constant to anything at all during development should be allowed and just do what needs to be done to make things work and let you keep debugging. Of course, it’s not a thing one should do during normal execution of a complete working program because it’s very expensive—which is why it should work but emit a warning. So not only do I think we should not disallow reassigning constants with the same type, I think we should go in the other direction and allow reassigning any value whatsoever to constants (with a warning). That’s what it means to take interactive development seriously.

Along those lines, I also think we should not emit a syntax error when break and continue occur outside of loops. This prevents cut-and-paste evaluation of code inside loops. It’s mostly fine that this errors because when this happens you’re manually simulating the control flow of the loop anyway, so you probably don’t need the evaluation of the expression with break or continue in it. But sometimes these expression have side-effects and we currently refuse to evaluate them, which can lead to confusion during REPL debugging. Instead I think that break and continue should be allowed—at least in the REPL—in top-level expressions and just error if they are evaluated.

14 Likes

I think a lot of the answer is that we have very different ideas about const globals. I view the prevelance of const globals as mostly a bad idiom that was necessary because we didn’t have syntax for typed globals. const globals have mainly been used because they are currently the only way to make global variables have acceptable performance. Since you get almost all the same optimizations from typed globals, I think they are probably the better pattern to use unless you have a very specific reason not to.

1 Like

I rather disagree with the premise that most constant global bindings should be something else. If you’re going to have globals, the best thing they can be is constant. That is by far the least problematic kind of global. A typed global is worse and is really just syntactic sugar for a constant binding to a type location. An untyped global is worse still.

6 Likes

I think that

In some cases changing the value of a const variable gives a warning instead of an error. However, this can produce unpredictable behavior or corrupt the state of your program, and so should be avoided. This feature is intended only for convenience during interactive use.

is a clear enough contract in Essentials · The Julia Language for an experienced user, but maybe it could be re-phrased to be more new user friendly.
I am not a native speaker so I might be biased in my understanding of this phrase, but it feels to me that mutating const is discouraged, but not very strongly (while I think new user should be strongly advised not to do it).

4 Likes

Even after #43671, reads from globals will still always be atomic, so while there might not be any type instabilities anymore, you probably still don’t want to use non-constant globals in performance sensitive contexts like tight loops. Constant propagation can also be quite crucial for performance in many cases, so I don’t think we want to encourage people to use typed globals over constants unless actually necessary.

4 Likes

Yeah, the notion that a typed global is a better version of a const global is just backwards… the const global is the best kind of global; typed globals should only be used where a global cannot be constant.

3 Likes

That happens to me quite a lot writing scripts. One example is solving a 1D differential equation, setting L, the size of the domain, to an integer, then needing a float later on. Or defining f(x) = ... then needing it for a frequency or something. But by far it occurs most because I tend to use only one julia REPL for all my projects, and when switching between projects I will have one variable with one meaning in a project and another in another.

Of course, it’s not a thing one should do during normal execution of a complete working program because it’s very expensive—which is why it should work but emit a warning

That’s still not a good solution for the very common (at least for me, and I would guess to a large number of people who write scripts, as opposed to modules) use of const as parameters in scripts. const always makes me super afraid I’m going to break julia, but I’ve literally never run into trouble with them. Can we guarantee that if you redefine all functions that use a particular const after redefining it then you’re good? If yes that could be a good solution.

The fact that many people want consts for different reasons makes me think there should be two notions of constness in the language. One proper const that you can never ever redefine in a julia session (for eg doing const MAGIC_NUMBER=42), and a lighter one to be used eg to declare parameters in scripts. The ideal semantics of that for me would be the following. When a variable is declared as lightconst, it gives functions licence to replace it with that value. If I do lightconst x = 42 then lightconst x = 43.3, that is allowed (without warning), and it is my responsability to make sure I redefine functions after evaluating it. If however I then do x = 43, that prints a warning, and undoes the lightconstness of x (which becomes a normal variable). That would allow me to define parameters at the top of my script, not have annoying warnings each time I include it, and have a nice behavior and warning when I change projects.

Yes, if you redefine all the methods that “capture” a constant after redefining the constant, then they’ll work fine.

This isn’t “light” though. This is exactly the thing that makes it possible for code to do potentially arbitrarily bad things if something that was supposed to be constant is redefined. There’s only two ways for a “constant” to be light:

  1. If it’s not actually assumed to be constant and only assumed to have the same type, which is exactly what globals with type annotations give you.

  2. If we were to keep track of which methods depend on which constant values and invalidate code when constants are redefined.

The former is exactly what we have now but “lightconst” is just a typed global. The latter is doubling down on allowing redefinition of constants, but that doesn’t make them lighter, it makes redefining them heavier. But I do think that would be good to support for interactive usage.

Is that statement documenting the current state of the compiler or the intended semantics? If the latter, could we make the warning be This may cause previously defined functions to fail or produce incorrect answers.? That’d be a lot nicer on my heart rate (assuming of course the intention of the warning is to inform the user of the risk and not scare them into not using const)

This isn’t “light” though

OK, light is probably a bad name for it, it’s more like unsafe_const.