Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Champion "readonly for locals and parameters" #188

Closed
5 tasks
gafter opened this issue Feb 26, 2017 · 1,183 comments
Closed
5 tasks

Champion "readonly for locals and parameters" #188

gafter opened this issue Feb 26, 2017 · 1,183 comments

Comments

@gafter
Copy link
Member

gafter commented Feb 26, 2017

  • Proposal added
  • Discussed in LDM
  • Decision in LDM
  • Finalized (done, rejected, inactive)
  • Spec'ed

See also dotnet/roslyn#115

Design review

@gulshan
Copy link

gulshan commented Mar 23, 2017

Any plan for readonly types(classes and structs)?

@Thaina
Copy link

Thaina commented Mar 27, 2017

should support

readonly i = 0; // shorthand for readonly var
const j = 0; // shorthand for const var

@jnm2
Copy link
Contributor

jnm2 commented Mar 27, 2017

Wasn't val going to be short for readonly var?

@Thaina
Copy link

Thaina commented Mar 28, 2017

@jnm2 I just don't like the idea of adding new keyword. Especially it is already have keyword in the language that has the same meaning

readonly might be a bit longer but we already preserved it from the start. We should reuse it. And to be shorter, just let we use readonly without var

At least I have seen some suggestion to use let that would still better than val because we already have it as keyword, even for linq scope

Especially because val was not keyword. I really have my code var val = 0; and I bet there are many people have val as variable or field name in their code like me. I think val is a bad choice

@jnm2
Copy link
Contributor

jnm2 commented Mar 28, 2017

@Thaina Yes, I'm inclined to agree.

@HaloFour
Copy link
Contributor

@Thaina

Especially because val was not keyword. I really have my code var val = 0; and I bet there are many people have val as variable or field name in their code like me. I think val is a bad choice

And you could continue to. Like var, val would be a contextual keyword, in that it only behaves like the keyword when it doesn't make sense for it to behave like anything else. So var val = 0; would remain perfectly legal.

Although I do prefer let to val, mostly because I think it looks sufficiently different.

@jnm2
Copy link
Contributor

jnm2 commented Mar 28, 2017

Oh yes! let was the one I liked. Thanks!

@Thaina
Copy link

Thaina commented Mar 28, 2017

@HaloFour It not breaking change I understand but it still ambiguous

BTW I still don't like let. I prefer readonly. But at least let is better than val

@benaadams
Copy link
Member

let is a bit early basic; also was read write. Not sure how let implies readonly?

@HaloFour
Copy link
Contributor

@benaadams

let is a bit early basic; also was read write. Not sure how let implies readonly?

let is also F# where it is the readonly (by default) binding of an identifier. let is also C# LINQ where it is the declaration of a readonly range variable.

Personally the latter reason is enough for me. It's already a contextual keyword, and in that existing context it creates a readonly identifier.

@benaadams
Copy link
Member

let is also C# LINQ where it is the declaration of a readonly range variable.

Fair enough 😄

@Richiban
Copy link

@gulshan

Any plan for readonly types(classes and structs)?

Do you mean immutable types? If so, that's a completely separate proposal (I'm pretty sure it's been made before).

@soroshsabz
Copy link

ITNOA

@Richiban where I can found it?

@Richiban
Copy link

@soroshsabz

ITNOA

That's a new one!

dotnet/roslyn#7626 and https://github.com/dotnet/roslyn/issues/159 are probably what you're looking for.

@HaloFour
Copy link
Contributor

HaloFour commented Mar 30, 2017

Something I like about final locals in Java is that it's possible to declare one as not assigned and then have branching logic to assign it. The Java compiler uses flow analysis to ensure that for each branch that the local is assigned exactly once.

final String name;
if (entity instanceof Person) {
    Person person = (Person)person;
    name = person.getFirstName() + " " + person.getLastName();
}
else if (entity instanceof Company) {
    Company company = (Company)company;
    name = company.getFirmName();
}

This can be useful in those scenarios where you want the local to be readonly, the expression to calculate it can throw and you want the scope of the local to exist beyond a try block.

@soroshsabz
Copy link

soroshsabz commented Mar 30, 2017

@Richiban thanks, but I hope to see comprehensive proposal about immutable object in csharplang project.

@soroshsabz
Copy link

@HaloFour Is conditional operator ( ?: ) not sufficient for this purpose?

@HaloFour
Copy link
Contributor

@soroshsabz

Sometimes not. I amended my comment to mention try/catch scenarios where C# offers no single expression. You could extract the logic to a separate function but that's more verbose ceremony. Even if it could be expressed as a single conditional expression sometimes it's more readable expanded out into multiple statements.

Either way, Java supports this, and I make use of it frequently enough that I think it would be useful here.

@soroshsabz
Copy link

soroshsabz commented Mar 30, 2017

I think simple rule like "All local readonly variables must be initializing immediately after declaration." cause to improve simplicity and readability for programmers. In Java case some programmers maybe surprise to final variable has not initialize and must be track code to find the where is this variable initialize?

@DavidArno
Copy link

@HaloFour,

That's yet another use-case for match:

let name = entity match (
    case Person person : $"{person.FirstName} {person.LastName}",
    case Company company : company.FirmName
);

@HaloFour
Copy link
Contributor

@soroshsabz

We may differ on opinion there. If the expression has to be overly complex in order to satisfy an overly strict language feature that only decreases overall maintainability and readability. I'd rather the flow be logical and the compiler enforce readonly-ness where appropriate.

And as a Java programmer who works directly with hundreds of other Java programmers I can say that this has never been a source of confusion. It's a pattern used fairly frequently across the Java ecosystem. If anything I think I would find it much more annoying that I couldn't declare and assign a readonly variable like this.

@DavidArno

It's just one exceptionally simple case. match won't handle the exception handling scenario. And again, forcing the developer to try to pack it all into a one-liner, or to extract that local logic elsewhere, does not improve the readability or maintainability of the program.

@DavidArno
Copy link

I lose track of the twists and turns of these arguments, but what is the reason why we can't treat this like NRTs and introduce an optional mut keyword. Don't enable the Immutable By Default (IBD) feature and nothing changes. Enable IBD and you'll get warnings on reassignments of variables that aren't marked mut. Seems by far the simplest and cleanest solution.

@CyrusNajmabadi
Copy link
Member

but still is considered to be a mistake (design) today

I don't consider it a mistake. I think it's fine and expected that method bodies be mutable. Within the method is where I do the work. Building up scratch data and whatnot.

In terms of numbers too, we see an insane amount of bugs caused by null (thousands in my career). But several orders of magnitude less for mutable variables. I think it's happened like once or twice in the same time.

I don't view it as a mistake. I view it as the normal, reasonable, effective way to code.

@CyrusNajmabadi
Copy link
Member

but what is the reason why we can't treat this like NRTs and introduce an optional mut keyword.

The normal reason that we don't like introducing dialects. We did it for nrt because the problem was huge and the gain was worth it. We don't see that with this issue. First, we don't even have data indicating there is a problem. So introducing a dialect for a non-issue seems unnecessary.

@colejohnson66
Copy link

Note: hidden struct copies are a performance hit and logical failure that is only solved by those who know they exist. Immutable structs by default would help there.

@DavidArno
Copy link

I don't view it as a mistake. I view it as the normal, reasonable, effective way to code.

First, we don't even have data indicating there is a problem

I refer you to the first quote. 😖

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Oct 3, 2024

I don't view it as a mistake. I view it as the normal, reasonable, effective way to code.

First, we don't even have data indicating there is a problem

I refer you to the first quote. 😖

Yes... what's the problem with teh first line. Mutation in methods is the normal, reasonable, and effective way to code. That's how we see methods in teh first place. They are the place where you do work, and that involves performing computation, storing values in temps, mutating as appropriate, then returning a result.

I'm a big fan of immutable values flowing between methods. But within a method, i don't see value in forcing readonlyness or immutability.

@HaloFour
Copy link
Contributor

HaloFour commented Oct 3, 2024

I don't disagree that mutability is a problem with an associated cost. But that cost is many, many times fewer orders of magnitude than the problems associated with null. That's why a dialect for NRTs was worth it and it's not for mutability.

The cost of mutability isn't a single value, either. A mutable local has extremely little cost associated with it. A captured mutable local is a different story, as now that mutable state could potentially be shared, although the scoping is quite limited. A captured primary constructor parameter has a much wider scope, so now you have the potential of shared state visible across multiple threads.

Say the LDM decided to just implement this feature, and allow readonly to be added to parameters and locals. What I would expect to happen is that some very small percentage would use it everywhere, making readonly by far the most common keyword used throughout those projects. A think another percentage would use it for very specific use cases, like primary constructor parameters. But I think the vast, vast majority wouldn't use it at all, because it's noise and it doesn't help their code in any way.

@tannergooding
Copy link
Member

Note: hidden struct copies are a performance hit and logical failure that is only solved by those who know they exist. Immutable structs by default would help there.

This isn't strictly correct and is entirely dependent on the size of the struct, how its used, and many other factors. Immutable structs likewise do not prevent copies, in fact they often do the exact opposite and encourage copies to be created instead.

For data that can be enregistered, immutable is often good and beneficial. This applies largely to ABI primitives such as int, long, float, double, etc; applies to some (but not all) simple wrapper structs, and to some other specialized data types like Vector128<T>. The exact characteristics are dependent on the underlying ABI (Application Binary Interface) and are per architecture/platform/calling convention. This type of data benefits from immutability because it is passed around and operated on in a register, not memory, so the cost of overwriting the entire struct is extremely low and often free.

However, for data that cannot be enregistered such as due to it being "too large" (typically anything over 2 * sizeof(nuint), but sometimes smaller types as well depending on many factors), the data is going to exist in memory regardless and the amount of tracking and other heuristics a compiler makes becomes extremely limited, especially across method boundaries. In such cases, immutability often causes more copies to be made because separate memory locations are needed and that necessitates a copy, especially if any kind references exist (of which there will often be plenty due to it not being enregisterable).

A lot of performant code therefore explicitly opts for mutability of locals accordingly, specifically because it allows more control of the storage locations and the developer to explicitly codify the semantics they intend. This can be especially beneficial in the face of enregistration or even field promotion. Appropriate usage of mutable locals, including parameters, is often key to optimizing core functions and limiting the amount of work the compiler has to do to understand the code.

@tannergooding
Copy link
Member

Now with that being said, some advanced immutable first languages (like Rust) do a lot of backend work to convert your seemingly immutable and functional-like code into a mutable, imperative model that suits how the computer actual operates. It does this by having first class integration of features and syntax that allows the compiler to better understand the application and do the type of advanced cross method optimizations that allow the efficient thing to actually happen and to happen safely.

@CyrusNajmabadi
Copy link
Member

I don't disagree that mutability is a problem with an associated cost. But that cost is many, many times fewer orders of magnitude than the problems associated with null. That's why a dialect for NRTs was worth it and it's not for mutability.

Exactly. I'm genuinely trying to find any data indicating there is any sort of significant issue with bugs due to non-readonlyness, or issues with understanding methods with mutable variables. I can't find anything indicating this is actually valuable for code. And, as has been mentioned a few times, this would now add a lot of noise for very little benefit. That is not a benefit to the language.

@vladd
Copy link

vladd commented Oct 3, 2024

I would argue that shallow immutablity (readonlyness) is useful mostly for the developer, in order to protect from silly mistakes. As far as yesterday I found in my code a mistake: I was modifying a wrong local variable by mistake inside some if-branch. Readonly would have prevented this bug.

So the advantage is not efficiency but ease of reasoning about the code.

@CyrusNajmabadi
Copy link
Member

in order to protect from silly mistakes.

Right. The issue is that we don't see this mistake happening much in the wild. As such, making a heavyweight, noisy, feature like this to prevent it is not palatable. We recommend that if this is not something you want, you write an analyzer for this purpose.

@vladd
Copy link

vladd commented Oct 3, 2024

Well, I cannot speak for the majority of C# users, but I find myself often in a position where I would prefer having readonlyness available. It must not be necessarily noisy, a simple let would do. However, I don't know is the implementation in the compiler is going to be heavyweight.

@Aniobodo
Copy link

Aniobodo commented Oct 3, 2024

in order to protect from silly mistakes.

Right. The issue is that we don't see this mistake happening much in the wild. As such, making a heavyweight, noisy, feature like this to prevent it is not palatable. We recommend that if this is not something you want, you write an analyzer for this purpose.

That scenario is more common than you think

@HaloFour
Copy link
Contributor

HaloFour commented Oct 3, 2024

It must not be necessarily noisy, a simple let would do.

Which can only ever solve for one particular use case, namely inferred locals. It doesn't help with parameters, and it doesn't help for anyone who prefers to not use type inference. There are no options to make it not noisy from the language perspective, aside creating a dialect.

@vladd
Copy link

vladd commented Oct 3, 2024

@HaloFour let int x = 42; is not too noisy as well.

@HaloFour
Copy link
Contributor

HaloFour commented Oct 3, 2024

@vladd

let int x = 42; is not too noisy as well.

The language team disagrees with you. It's an additional modifier which, for the folks that want this feature, would end up added to the vast majority of all declarations. That is noisy. The fact that it's 3 characters instead of 8 doesn't really change that.

@vladd
Copy link

vladd commented Oct 3, 2024

@HaloFour If the alternative is no readonly for locals at all, I would prefer "noisy" 3 characters. Perfect is the enemy of good.

@vladd
Copy link

vladd commented Oct 3, 2024

@HaloFour
I wonder why does language team think that extra 3 characters is an unbearable noise, whereas culture-invariant string interpolation has to be encoded in a really inelegant way string.Create(CultureInfo.InvariantCulture, $"it was {value}");
</offtopic>

@HaloFour
Copy link
Contributor

HaloFour commented Oct 3, 2024

If the alternative is no readonly for locals at all, I would prefer "noisy" 3 characters. Perfect is the enemy of good.

The alternative is an analyzer, so you can choose whether you want locals to be treated as readonly by default, as an analyzer is allowed to make decisions that the language won't.

@vladd
Copy link

vladd commented Oct 3, 2024

@HaloFour Analyzer is a poor man's alternative (which is, well, better than nothing). This alternative would create a non-standard language dialect, developers will need to learn how to apply the needed analyzer, how to fight its bugs, how to cancel the readonlyness for cases where it's not intended and so on. In some organizations this would create an additional SOUP item.

@HaloFour
Copy link
Contributor

HaloFour commented Oct 3, 2024

@vladd

It doesn't create a dialect, it limits what you can do within the standard language. This is no different from StyleCop or any of the other tools routinely used by organizations to enforce code hygiene. It's infinitely cheaper than a language change and addresses all of these concerns, including the ability to flip the default, or to enforce in some situations and not others.

@333fred
Copy link
Member

333fred commented Oct 3, 2024

In the interest of making the language team's position on this clear, we've decided to close this issue, mark it Likely Never, lock it, and are moving the existing comments over to a discussion. It is our position at this time that readonly locals are not going to be added to the language; we do not feel that the noise from adding readonly as a local modifier, which will then need to be applied to most local variables, is worth it for the benefits that such a change would bring. We also do not believe that benefits here would be worth introducing a compiler switch or language dialect. readonly on parameters is still an open question, and we will track that specific proposal here. If we someday change our minds on readonly locals, we will create a new champion issue at that point.

@333fred 333fred closed this as completed Oct 3, 2024
@333fred 333fred closed this as not planned Won't fix, can't repro, duplicate, stale Oct 3, 2024
@dotnet dotnet locked and limited conversation to collaborators Oct 3, 2024
@333fred 333fred converted this issue into discussion #8479 Oct 3, 2024
@333fred 333fred modified the milestones: Backlog, Likely Never Oct 3, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests