C# Design Notes for Dec 16, 2013

Topics: C# Language Design
Coordinator
Mar 29, 2014 at 2:41 PM
Edited Mar 29, 2014 at 3:43 PM

C# Design Notes for Dec 16, 2013

Notes are archived here.

Agenda

This being the last design meeting of the year, we focused on firming up some of the features that we’d like to see prototyped first, so that developers can get going on implementing them.
  1. Declaration expressions <reaffirmed scope rules, clarified variable introduction>
  2. Semicolon operator <reaffirmed enclosing parentheses>
  3. Lightweight dynamic member access <decided on a syntax>

Declaration expressions

On Sep 23 we tentatively decided on rules for what the scope is when a variable is introduced by a declaration expression. Roughly speaking, the rules put scope boundaries around “structured statements”. While this mostly makes sense, there is one idiom, the “guard clause” pattern, that falls a little short here:
if (!int.TryParse(s, out int i)) return false; // or throw, etc.// code ideally consuming i
Since the variable i would be scoped to the if statement, using a declaration statement to introduce it inside of the if would not work.

We talked again about whether to rethink the scope rules. Should we have a different rule to the effect essentially of the variable declaration being introduced “on the preceding statement”? I.e. the above would be equivalent to
int i;
if (!int.TryParse(s, out i)) return false; // or throw, etc.// code consuming i
This opens its own can of worms. Not every statement has a preceding statement (e.g. when being nested in another statement). Should it bubble up to the enclosing statement recursively? Should it introduce a block around the current statement to hold the generated preceding statement, etc.

More damning to this idea is the issue of initialization. If a variable with an initializer introduced in e.g. a while loop body is understood to really be a single variable declared outside of the loop, then that same variable would be re-initialized every time around the loop. That seems quite counterintuitive.

This brings us to the related issue about variable lifetimes. When a variable is introduced somewhere in a loop, is it a fresh variable each time around? It would probably seem strange if it weren’t. In fact we took a slight breaking change in C# 5.0 in order to make that the case for the loop variable in foreach, because the alternative didn’t make sense to people, and tripped them up when they were capturing the variable in lambdas, etc.

Conclusion

We keep the scope rules described earlier, despite the fact that the guard clause example doesn’t work. We’ll look at feedback on the implementation and see if we need to adjust. On variable lifetimes, each iteration of a loop will introduce its own instance of a variable introduced in the scope of that loop. The only exception is if it occurs in the initializer of a for loop, because that part is evaluated only on entry to the loop.

Semicolon operator

We previously decided that a sequence of expressions separated by semicolons need to be enclosed in parentheses. This is so that we don’t get into situations where e.g. commas and semicolons are interspersed among expressions and it isn’t visually clear what the precedence is.

We discussed briefly whether those parentheses are necessary even when there is other bracketing around the semicolon-separated expressions.

Conclusion

It’s not worth having special rules for this. Instead, semicolons are a relatively straightforward addition to parenthesized expressions – something like this:
parenthesized-expression:
  • ( expression-sequence_opt expression_ )
expression-sequence:
  • expression-sequence_opt statement-expression ;
  • expression-sequence_opt declaration-expression ;

Lightweight dynamic member access

On Nov 4 we decided that lightweight member access should take the form x.<glyph>Foo for some value of <glyph>. One candidate for the glyph is #, so that member access would look like this:
payload.#People[i].#Name
However, # plays really badly with preprocessor directives. It seems almost impossible to come up with syntactic rules that reconcile with the deep lexer-based recognition of #-based preprocessor directives. After searching the keyboard for a while, we settled on ‘$’ as a good candidate. It sort of invokes a “string” aspect in a tip of the hat to classic Basic:
payload.$People[i].$Name
One key aspect of dynamicness that we haven’t captured so far is the ability to declaratively construct an object with “lightweight dynamic members”, i.e. with entries accessible with string keys. A core syntactic insight here is to think of $Foo above as a unit – as a “lightweight dynamic member name”. What this enables is to think of object construction in terms of object initializers – where the member names are dynamic:
var json = new JsonObject 
{ 
    $foo = 1,
    $bar = new JsonArray { "Hello", "World" },
    $baz = new JsonObject { $x = 1, $y = 2 }
};
We could also consider allowing a free standing $Foo in analogy with a free standing simple name being able to reference an enclosing member in the implicit meaning of this.$Foo. This seems a little over the top, so we won’t do that for now.
One thing to consider is that objects will sometimes have keys that aren’t identifiers. This is quite common in Json payloads. That’s fine as far as member access is concerned: just fall back to indexing syntax when necessary:
payload.$People[i].["first name"]
It would be nice to also be able to initialize such “members” in object initializers. This leads to the idea of a generalized “dictionary initializer” syntax in object initializers:
var payload = new JsonObject
{
    ["first name"] = "Donald",
    ["last name"] = "Duck",
    $city = "Duckburg" // equivalent to ["city"] = "Duckburg"
};
So just as x.$Foo is a shorthand for x["Foo"] in expressions, $Foo=value is shorthand for ["Foo"]=value in object initializers. That syntax in turn is a generalized dictionary initializer syntax, that lets you index and assign on the newly created object, so that the above is equivalent to
var __tmp = new JsonObject();
__tmp["first name"] = "Donald";
__tmp["last name"] = "Duck";
__tmp["city"] = "Duckburg";
var payload = __tmp;
It could be used equally well with non-string indexers.
A remaining nuisance is having to write all the type names during construction. Could some of them be inferred, or a default be chosen? That’s a topic for a later time.

Conclusion

We’ll introduce a notion of dictionary initializers [index]=value, which are indices enclosed in […] being assigned to in object initializers. We’ll also introduce a shorthand x.$Foo for indexing with strings x["Foo"], and a shorthand $Foo=value for a string dictionary initializer ["Foo"]=value.
Apr 8, 2014 at 7:33 AM
Edited Apr 8, 2014 at 7:36 AM
payload.$People[i].["first name"]

Some questions:
  1. Dot before ["first name"] is required?
  2. Why there is no lightweight syntax for ["first name"]? For example .$"first name". This syntax is more consistent and readable.
Coordinator
Apr 14, 2014 at 6:16 PM
Thanks for pointing these out:
  1. The dot before ["first name"] were a typo. The example should read payload.$People[i]["first name"].
  2. By the time you have to quote the name it no longer feels lightweight. At that point you may as well index using square brackets. At least that's how we felt about it. If one day we come up with a way to allow member names with spaces in general, we would apply it also in conjunction with $. But that's not for right now.
Apr 16, 2014 at 6:04 AM
Thanks, clear.

But after reading https://roslyn.codeplex.com/discussions/541566 also arises question about benefit from lightweight syntax:
1. payload["People"][i]["first name"]
2. payload.$People[i]["first name"]
3. payload.$People[i].${first name} // nonexistent syntax
4. payload.$People[i].$"first name" // nonexistent syntax
For me first version is more plain and understandable.