-
Notifications
You must be signed in to change notification settings - Fork 1k
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 "static local functions" (16.3, Core 3) #1565
Comments
To be clear, would this fit @benaadams's definition of
|
@bondsbw Yes, that's what I'd have in mind. The local function would not be able to access variables from parent method or instance fields. It could only access variables that it declares (parameters or locals) and static fields. |
Is there a big reason to do this for local functions, aside avoiding accidental mutations? Aren't local functions significantly more efficient with captures? |
@HaloFour The primary motivation for this proposal is readability/maintainability. Knowing that some code is isolated makes it easier to understand, review and re-use. That's the same general argument as with any kind of isolation like object encapsulation or functional programming. There is also a practical motivation. There are good reasons why parameters or locals of local functions should not shadow variables from the enclosing scope. But in practice, there are many pieces of pure logic that make sense to extract as local function, but where this restriction becomes inconvenient (prevents you from using obvious variable names). |
What’s the Benefit over a private static function? Local functions benefit is exactly capturing |
@popcatalin81 more fine-grained access control, primarily |
@HaloFour it isn't just mutations; it is also to incredibly useful to avoid accidentally creating the capture machinery, especially in hot paths. This can have a surprising amount of overhead, especially in scenarios like @popcatalin81 scoping and expressiveness. I actually strongly disagree with you that the benefit of local functions is "exactly capturing" - I've written plenty of local functions, and the ones that make use of capture are by far the minority. Well, let me rephrase: the ones where I intentionally used capture are the minority. But yes, functionally they're just like regular |
If the goal is to avoid creating async machinery for capturing then I don't see why it would be limited to avoiding capture on local functions where said machinery requires significantly less overhead. And, like most proposals that involve limiting functionality/syntax for the purpose of improving code generation and performance, why is this not suitably managed by an analyzer? Most people will not care about this degree of optimization. I've never reached for a local function unless I wanted to capture. I see zero point to them otherwise. |
IMO, those two statements are in direct contradiction. Allowing a local function to shadow variables would negatively impact the readability of said function. Now anytime you see a local function you have to double-back to the signature to see what that identifier refers to. |
@HaloFour Any design involves some trade-offs and tensions. I recognize it is a fair concern to discuss and experiment with. |
I agree. Consider this just part of that discussion. 😁 Despite my arguments I'm fairly ambivalent about the proposal. Where this feature tries to address performance of certain aspects of generated code, particularly around I'm much more opposed to the notion of the shadowing. I agree that having to come up with a lot of different names for identifiers can be restrictive. The team was very quick to dismiss that argument in the context of leaky
Current static methods don't have to deal with other locals being potentially in scope, so I don't feel that this comparison is appropriate. The rules regarding shadowing fields don't change between instance or static methods. |
You can conceptually map captures to instance fields, class C
{
int name = 0;
void Method1() => Use(name); // OK
static void Method2() => Use(name); // Error
}
void M()
{
int name = 0;
void Local1() => Use(name); // OK
static void Local2() => Use(name); // Error
} Also, methods can shadow fields with parameters and locals regardless of being static. |
@HaloFour To me, shadowing is a feature. I occasionally shadow deliberately in order to prevent uncontrolled access to a field; I've long been wishing I could declare local function parameters that shadow locals that would otherwise have been captured, in order to prevent their capture and accidental use out-of-band, while still capturing others or simply while declaring a fully-static helper method closer to where it is relevant. |
For me this has nothing to do with improving code generation, it's all about improving code readability. Local functions are making it much more common to have top level functions which are several hundred lines long. The logic gets split between the top level method body and a number of local functions whose behavior is specific to the top level method. Before local functions such code would be split into a number of top level method bodies.and the helper functions would be needlessly polluting the containing types scope. Now the entirety of the logic can be contained with in the top level method body. Such local functions are harder to review than they would be as a top level function. The silent capture of state can affect how they execute and must be accounted for. There is nothing inherently wrong with this and many times it's hugely beneficial to the developer. In the cases where no capture is done, or when the developer specifically wants to avoid it, the For small functions this really isn't that important. But in larger methods it can make a huge difference. It does have the additional benefit of ensuring local functions are capture free and hence will be a tiny bit more efficient as we don't even have to pass a struct closure around and have local indirection. If that were the primary motivation of this feature I'd be against it. Not worth the cost IMHO.
I'm still a bit on the fence about this. In the absence of a static modifier I'd be 100% against shadowing. It creates too much opportunity subtle behavior changes. Imagine you delete a parameter that shadows a local in the top level method, oops now you just introduced a capture you probably didn't intend. Restricting shadowing to local functions marked as static though removes this problem and most of the issues around shadowing. It also makes local functions significantly more usable. I didn't really appreciate this problem until I started using local functions a lot in C#. |
I don't think that's the intuition that most people have regarding captured locals.
Yes, but never without a way to explicitly refer to that shadowed field, and you're always required to explicitly qualify the field anytime such a local is in scope.
Sounds like a good argument for removing local functions from the language. 😉 But seriously, cramming hundreds of lines into a single method definition doesn't sound like a good idea, with or without local functions. In my opinion that's exactly when those chunks of logic should be top-level functions, specifically so that they can maintained and tested in isolation from one another. I can't fathom a local function that's more than maybe 4-5 LOC, and I would count that against the LOC of the function in which its contained.
Which is probably why they shouldn't be local functions, regardless of whether they capture. Locality isn't an excuse to hide a massive ball of logic behind a blackbox.
The technical problems, maybe. But I still see the problem of having to mentally parse through the contents of the local function and upon seeing any given identifier having to double-back to the signature to see whether the lexical scoping has been reset. IPU forbid having to deal with that when nesting local functions (which I hope ain't becoming a thing.) It's not that |
I used to believe the same. Then I spent a bunch of time coding in F# which has local functions and gradually I opened up to the advantages it provides. There is value in keeping all the logic necessary for an operation located together.
The reverse is polluting a type with a number of methods that are either a) useful to a single method or worse b) only legal to be called from a specific method. Over times types grow and local functions help avoid polluting types with a number of members that simply aren't generally useful. When I type
That is true today without local functions. In addition to a parameter or local the value |
This has MANY speed advantages. We could declare a static lambda like this:
|
The proposal above doesn't touch on lambdas, although they are probably worth bringing up. You really wouldn't be able to avoid the allocation of the delegate itself, particularly if the delegate instance escaped the method in which it was declared. |
Only for lambdas which this proposal doesn't currently cover. It's a natural extension to consider but the focus for now is on local functions. For local functions there really isn't a significant speed up static vs. non static in the general case. |
I think this thread presents a better case for improved capturing mechanisms of local functions rather than static modifier. Example: instead of capturing in a compiler generated class the following could be viable
This does not give perfect control to those who whish it, but makes refactoring a lot easier compared to static non static local function. As a second benefit, even local functions that are not marked static will benefit performance wise, by eliminating captures when possible. |
Static delegates are semi-related (both to the static lambda discussion above and to the static local functions here): https://github.com/dotnet/csharplang/blob/master/proposals/static-delegates.md |
What about when you want to disable capturing but still have access to instance fields? |
@JamesNK I can see the merit of a "no capture, but instance" scenario... You're right in that this aspect of the proposal is being influenced - perhaps incorrectly - by the tempting convenience of that Thinking pragmatically, perhaps |
If a local function is not assigned to a delegate, a non-allocating capturing mechanism is already used. Though it's not done the way you suggest, instead, a |
@svick |
Related Design Notes: https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-09-10.md |
Seems to be available in VS 16 preview 2, using |
Secret feature?? 🤤 |
If you guys end up adding this feature to the language, maybe local extension methods are worth considering too? It's not a must have thing but it does sound like a nice to have thing. The syntax would be similar to the classic extension methods: bool CanPairUp(Token t1, Token t2)
{
if (t1.IsKeywordOrIdentifier() && t2.IsKeywordOrIdentifier())
{
return false;
}
return true;
bool IsKeywordOrIdentifier(this Token t)
{
return t.Kind == TokenKind.Keyword || t.kind == TokenKind.Identifier;
}
} |
@TKharaishvili local extension method sis something we've considered but at this point don't have firm plans for. I think we'd need to see compelling scenarios to add the complexity to member lookup here. Extension methods solve a couple of problems:
In the case of local functions there is no discovery issue. The behavior is right there in the same method that you are editing. Can't even use |
The benefits of this being method chaining and the fact that it reads better. |
It seems like implementing this undoes most of the arguments against static local variables. |
Could you clarify? Other than sharing part of the name |
No... but it's rather confusing at first glance until you think about how a regular static method is invoked and then you realize it doesn't matter, it just saves someone else from outside the function being able to easily call it from outside the scope and thus wouldn't pollute intellisense, as well as give you access to statics... |
Are there any discussions about local property? Is it also included in local function? So we can define getter and setter with one shared signature in the function |
Iirc there have been other discussions about more local things, like types and properties. They haven't gone anywhere. |
The proposal is to declare a local function as non-capturing. The keyword to indicate that would be
static
.Note: if we do this, then we should also consider allowing parameters and locals of the local function to shadow parameters and locals from the containing method.
Related discussion thread, including a discussion of "capture lists" (a more elaborate feature): #1277
Note: EE scenarios are a concern.
Do we want to allow some other modifiers (like
unsafe
)?LDM history:
The text was updated successfully, but these errors were encountered: