Skip to content
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

Language feature: Static Expressions #10506

Closed
GSPP opened this issue Apr 12, 2016 · 16 comments
Closed

Language feature: Static Expressions #10506

GSPP opened this issue Apr 12, 2016 · 16 comments

Comments

@GSPP
Copy link

GSPP commented Apr 12, 2016

Sometimes, you want to compute a value once and cache it for the rest of the application's lifetime. Common cases are:

  • Compiled Regex instances
  • Initialization from configuration
  • Expensive computations e.g. the prime numbers table in Dictionary and Hashtable
  • Singletons such as an empty array or an empty MemoryStream

Currently, you'd do this:

static readonly Regex emailRegex = GetEMailRegex();

static bool IsMatch(string str) {
 return emailRegex.IsMatch(str);
}

This code is not ideal because although the variable and the function are directly related they often end up being physically at separate places in the source.

This would be more convenient:

static bool IsMatch(string str) {
 return static(GetEMailRegex()).IsMatch(str);
}

And static(expression) would automatically convert that expression into a static readonly variable.

An alternative feature would be to allow placing the static variable into a method. This would not change anything about semantics but would allow developers to group their code better. The variable travels with the code that uses it. C++ has this feature.

I often find myself thinking "This expression is a constant. I could cache it but I won't bother creating the variable and causing more work for myself in the future. I'll just new up that object constantly to have an easier time programming.".


Collection of cases:

static bool IsMatch(string str) {
 return static(new Regex(@".*@.*", RegexOptions.Compiled)).IsMatch(str);
}

static T2 ConvertAll<T1, T2>(T1[] array, Func<T1, T2> converter) {
 if (array.Length == 0) return static (new T2[0]);
 //...
}

static Task FlushSomeBufferAsync() {
 if (buffer.IsEmpty) return static (Task.FromResult((object)null));
 return FlushInnerAsync();
}

static ProductInfo GetProductToAdvertise(...) {
 if (...) return static (new ProductInfo(name: "Milk", price: 1m));
 else if (...) return static (new ProductInfo(name: "Tomatoes", price: 2.3m));
 else return new ProductInfo(name: "SQL Server", price: 8000m * cpuCoreCount); //not static, dependent on args
}
@HaloFour
Copy link

Seems very related to the half-dozen or so lazy/cached initialization proposals already made: #3630, #524, #3251, #4374, #3415.

What exactly are the semantics? I assume that the result of the expression is promoted to a field, but is that a static field or an instance field? Might that depend on where that expression is used or where it is defined? How about atomicity?

I will note that while VB.NET uses the Static keyword for local variables which retain their value between invocations that syntax is legacy. C#'s static keyword has a very different meaning and I don't think that it would make much sense to reuse it in such a context.

@GSPP
Copy link
Author

GSPP commented Apr 12, 2016

Wow, it's indeed half a dozen :) The first two are for fields only. The 3rd is for a synchronized lazy expression which I am not proposing. Number 4 is kind of unrelated. Number 5 is for lazy fields.

I propose semantics exactly identical to a static readonly field since I don't see any problems there (design, effort, performance). The definition of what's legal is what's legal in such a field. Initialization time and synchronization uses the same semantics.

@alrz
Copy link
Member

alrz commented Apr 12, 2016

static T2 ConvertAll<T1, T2>(T1[] array, Func<T1, T2> converter) {
 if (array.Length == 0) return static (new T2[0]);
 //...
}

You can't use method type parameters.

@GSPP
Copy link
Author

GSPP commented Apr 12, 2016

@alrz good catch! Maybe we can fix this if the compiler generates a hidden static class that has type arguments. I think it should do that anyway so that the static readonly fields always can use beforefieldinit semantics (no static ctor defined).

This would also work in the following case:

static readonly int manuallyDefinedField = 1234;
static F() => static (manuallyDefinedField + 1);

Because the generated class can use the manually defined statics from the outer class. Statics introduced by static() might be dependent on manually defined statics but not vice versa.

@GSPP
Copy link
Author

GSPP commented Apr 12, 2016

Open questions:

  1. Is deduplication is supposed to be performed? (static (F()) + static(F()))
  2. Is constant expression evaluation is allowed (static(1) =>1`)?
  3. What do nested statics means (static (static (F()))), probably disallowed.

@alrz
Copy link
Member

alrz commented Apr 12, 2016

@GSPP VB doesn't allow this (32068) but it does some acrobatics to catch method parameters etc.

Anyhow, considering that static fields would never GC'd I think this encourages to making a lot of things wrapped in static and you will need to literally scan the whole class to know how much memory it actually uses. So I'd rather use the "make field" code fix when I'm not in the mood to write it by hand.

@GSPP
Copy link
Author

GSPP commented Apr 12, 2016

Understandable. My counter argument to that is that fields tend to be placed at unrelated points in the source code. Many code styles demand ordering members by type. I personally don't view this as a useful organization scheme. It is rare that someone needs to see all fields or all methods in one place. It is more common to want to see everything related to one feature. So I like to place all members of one feature together. So far the tooling is always working against me with that.

@alrz
Copy link
Member

alrz commented Apr 12, 2016

That's true for properties' backing fields, but static fields have a one-to-one relationship with the type itself so it would make more sense to declare static fields (at least) at the top regardless of where they are actually being used.

@gafter
Copy link
Member

gafter commented Apr 17, 2016

I'd rather do #10552.

@FrankHB
Copy link

FrankHB commented Apr 18, 2016

I don't think that static is a legacy for local variables, but for variables in some other scopes. In C and some derived languages, static basically (literally) means static storage/duration class/category, which makes the designated entity live within the whole program. On the other hand, the identity of the denotation (from name to variable) is guaranteed by scoping and linkage rules. The true legacy use is to indicate internal linkage of names in file/namespace scope, particularly on functions (which has no sense to talk about the storage). Note that in C/C++, a file/namespace scope variable does have the static storage class/category by default (even without static), this often causes more confusion but should have been avoided here. The one-to-one mapping from the enclosing class to the identity of the denotation for static class members (note: also not only for ones which have sense to talk about the storage) is a side effect from the literal meaning of the keyword, which shows some similarity between classes and namespaces and also sounds "legacy" in a similar flavor: why "static" while the meaning is not only about the storage? It seems the misconception of etymology has already been widely adopted and you can't easily to correct it without some other changes in these languages.

@aluanhaddad
Copy link

@GSPP Just to play devil's advocate, you can currently write

static Func<string, bool> IsMatch { get; } = GetEmailRegex().IsMatch;

From my point of view, the problem with using static, no pun intended, is that it is not clear what the scope would be for static methods vs instance methods. All of your examples involve static methods, but is the generated field intended to be shared among instances if it is an instance method? What about among instantiations of generic types? I understand that it would be readonly but given that this is in no way an immutability guarantee it would make a difference.

@GSPP
Copy link
Author

GSPP commented Apr 24, 2016

@aluanhaddad all semantics are exactly identical to a static field declared in the enclosing type. From that all answers follow: One value per generic instantiation and the value is shared across instances.

I find these semantics super easy to understand.

@aluanhaddad
Copy link

@GSPP you answered my question which was whether or not instances shared the static value. A lot of use cases for cached properties involve having one cached value per instance. Would there be no way with this proposal to have an instance property or method backed by a per instance cached field? If that were allowed, the static syntax would be ambiguous.

@GSPP
Copy link
Author

GSPP commented Apr 29, 2016

@aluanhaddad there is no proposal for cached instance state by me. This seems hard to get right. I believe there are multiple other tickets about that, see #10506 (comment).

@aluanhaddad
Copy link

@GSSP Roger, static expression apply to static members only.

@gafter
Copy link
Member

gafter commented Mar 24, 2017

We are now taking language feature discussion in other repositories:

Features that are under active design or development, or which are "championed" by someone on the language design team, have already been moved either as issues or as checked-in design documents. For example, the proposal in this repo "Proposal: Partial interface implementation a.k.a. Traits" (issue 16139 and a few other issues that request the same thing) are now tracked by the language team at issue 52 in https://github.com/dotnet/csharplang/issues, and there is a draft spec at https://github.com/dotnet/csharplang/blob/master/proposals/default-interface-methods.md and further discussion at issue 288 in https://github.com/dotnet/csharplang/issues. Prototyping of the compiler portion of language features is still tracked here; see, for example, https://github.com/dotnet/roslyn/tree/features/DefaultInterfaceImplementation and issue 17952.

In order to facilitate that transition, we have started closing language design discussions from the roslyn repo with a note briefly explaining why. When we are aware of an existing discussion for the feature already in the new repo, we are adding a link to that. But we're not adding new issues to the new repos for existing discussions in this repo that the language design team does not currently envision taking on. Our intent is to eventually close the language design issues in the Roslyn repo and encourage discussion in one of the new repos instead.

Our intent is not to shut down discussion on language design - you can still continue discussion on the closed issues if you want - but rather we would like to encourage people to move discussion to where we are more likely to be paying attention (the new repo), or to abandon discussions that are no longer of interest to you.

If you happen to notice that one of the closed issues has a relevant issue in the new repo, and we have not added a link to the new issue, we would appreciate you providing a link from the old to the new discussion. That way people who are still interested in the discussion can start paying attention to the new issue.

Also, we'd welcome any ideas you might have on how we could better manage the transition. Comments and discussion about closing and/or moving issues should be directed to #18002. Comments and discussion about this issue can take place here or on an issue in the relevant repo.

I am not moving this particular issue because I don't have confidence that the LDM would likely consider doing this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants