Null conditional assignment is bunk.

When you have an expression P which names a mutable place, and you execute P := X, the contract says that P now exhibits the value X, until it is assigned another value.

Conditional assignment fucks this up. When P doesn't exist, X is not stored. (Worse, it may even be that the expression X is not evaluated, depending on how deep the fuckery goes.)

Then when you access the same expression P, the conditional assignment becomes conditional access and you get back some default value like a nil.

Store X, get back nil.

That's like a hardware register, not a durable memory model.

It's okay for a config.connection?.retryPolicy to come up nil when there is no config.connection. It can be that the design makes nil a valid retry policy, representing some default. Or it could be that it is not the case, but the code which uses connection? handles the nil soon afterward.

But a store to config.connection?.retryPolicy not having an effect; that is dodgy.

What you need for config.connection? to do when the expression is being used to calculate a mutable place to assign to is to check that config.connection is null, and in that case, instantiate a representative instance of something which is then assigned to config.connnection, such that the config.connection.retryPolicy place then exists and the assignment can proceed.

This recognizable as a variation on COW (copy-on-write); having some default instance for reading, but allocating something on writing.

In a virtual memory system, freshly allocated memory can appear to contain zero bytes on access due to all of its pages being mapped to a single all-zero frame that exists in the entire system. Conceptually, the hardware could do away with even that all-zero frame and just have a page table entry which says "this is a zero-filled page", so the processor then fakes out the zero values without accessing anything. When the nonexistent page is written, then it gets the backing storage.

In order to instantiate settings.connection? we need to know what that has to be. If we have a static type system, it can come from that: the connection member is of some declared type of which a representative instance can be produced with all constructor parameters defaulted. Under a dynamic paradigm, the settings object can have a handler for this: a request to materialize a field of the object that is required for an assignment.

If you don't want a representative config.connection to be created when config.connection?.retryPolicy is assigned, preferring instead that config.connection stays null, and the assignment is sent to the bit buckets, you have incredibly bad taste and a poor understanding of software engineering and programming language design --- and the design of your program is scatter-brained accordingly.

> When you have an expression P which names a mutable place, and you execute P := X

This isn't the case, though, is it? A normal member access (or indexer) expression may point to a mutable location (field, property). However, with conditional access expressions you get either a member access _or nothing_. And that nothing is not a mutable place.

When you use any of the conditional operators, you split the following code into two paths, and dropping the assignment (since there's nothing to assign to) seems pretty consistent to me, since you'd also drop an invocation, or a property evaluation in similar expressions.

If you require P to point to something that actually exists because you want the assignment to succeed, then write code to ensure that P exists because the assignment has no way of knowing what the intention was on the left side.

If the language allows <whatever> := <new-value> --- if that compiles and executes --- then <whatever> is by definition a mutable place.

Once you introduce this misfeature, mutable places no longer have the property that they all record the assigned value and keep it until the next assignment.

Now, sure, you also don't have that property when you have operator overloading that allows assignment to be coded by the programmer; but that's a design decision under the programmer's control which affects only that code base, not the entire language and all its users.

A representative config.connection being made out of nothing sounds pretty bad to me. If you want to make sure the value doesn't disappear, you shouldn't be using conditional assignment in the first place.

The config example isn't the best, but instead imagine if it was just connection.?retryPolicy. After you set connection?.retryPolicy it would be weird for reading it back to be null. But it would be just as weird for connection?.retryPolicy to not be null when we never established a connection in the first place.

The copy on write analogy is tempting but what you're describing only works when the default value is entirely made of nulls. If you need anything that isn't null, you need to actually make an object (either upfront or on first access). And if you do that, you don't need ?. anymore.

> If you want to make sure the value doesn't disappear, you shouldn't be using conditional assignment in the first place.

If it worked using "materialize-on-write" semantics, why wouldn't you, as an alternative to the verbose code which checks every path component that might not exist, and instantiates it before doing the assignment?

Obviously, you can't use it if you don't have materialize-on-write semantics in the assigned expression not that you shouldn't.

If it materialized instead, yeah that sentence wouldn't apply the same way. But materializing with a default constructor opens up many cans of worms. I would say it has significantly more downsides than short circuiting. Also it should use a different operator.

Small additional point: if it used a different operator, what if that operator was used in an expression that is not the target of an assignment? Does that operator just become equivalent to ?, or does it do the materializing anyway even though it is not required in support of an assignment?

It would probably materialize no matter what.

But syntax error would be fine.

Definitely not acting the same as a question mark.

Someone is going to run into a null exception in an assignment and just throw in the question mark to shut it up, not thinking about the value disappearing.

That's the mindset the feature is developed for (and by).

This is syntactic sugar only. No change in semantics. So I don't buy into any argument about what this change 'means'.

And we all get to choose what we find ridiculous:

i = i + 1 ? No it does not. Never has, never will.

Connection is null? It's insane to type it as Connection then. null has type Null.

I don't understand what you are talking about.

The syntactic sugar already exits before this change. That is to say, in a version f C# without the feature, you can write this:

  a.b?.c = foo
It's not not well-formed semantically.

What the change does is allow the above to be well-formed semantically. If b isn't null, then it behaves like a.b.c = foo. Otherwise, the value of foo is discarded. (Perhaps foo isn't even evaluated?!)

The idea that there is no change in semantics, but only syntactic sugar is exactly backwards.

A particular meaning was assigned to combinations of syntactic sugar which were previously invalid.

That meaning involved making a design choice among multiple possible meanings.

Is there any public visibility to the design decision; what alternatives were considered for the semantics and rejected?

> The idea that there is no change in semantics, but only syntactic sugar is exactly backwards.

> The syntactic sugar already exits before this change. That is to say, in a version f C# without the feature, you can write this:

  a.b?.c = foo
You cannot.

The semantics is conditional assignment, which already exists in the language. Hence the article repeatedly bludgeoning you with pairs of code snippets - whose semantics is the same - but there is a new syntax. So maybe you could say "there's no syntax change" in the sense that "there is only additional syntax, none of the old syntax as been modified".

When you insisted this was a change in semantics, I double-checked, because this would be a huge fuckup (and would explain why you're making such a fuss about it). A difference in semantics (in this case) would mean backward-incompatibility, and would break all codebases already using the syntax a.b?.c = foo. But old codebases do not use that syntax because that syntax does not exist before 14. Old codebases already do conditional assignment, which has not changed. After 14 they will have new syntactic sugar to carry out the old semantics.

This is what sibling comment meant semantically with "Apparently you do t use if in your code?" even if his syntax ("do t") was fucked up.

> Is there any public visibility to the design decision; what alternatives were considered for the semantics and rejected?

Asked about for years I guess: https://stackoverflow.com/questions/35887106/using-the-null-... (I.e. their semantics already works, but they want a better syntax (or sugar) to carry out the same semantics.)

And the discussion: https://github.com/dotnet/csharplang/discussions/6072

Apparently you do t use if in your code?