dotnet / csharplang

The official repo for the design of the C# programming language
11.56k stars 1.03k forks source link

Proposal: Use static keyword for lambdas to disallow capturing locals and parameters (VS 16.8, .NET 5) #275

Open mattwar opened 7 years ago

mattwar commented 7 years ago

Allow Func<int, string> = static i => i.ToString();.


To avoid accidentally capturing any local state when supplying lambda functions for method arguments, prefix the lambda declaration with the static keyword. This makes the lambda function just like a static method, no capturing locals and no access to this or base.

int y = 10;
someMethod(x => x + y); // captures 'y', causing unintended allocation.

with this proposal you could the static keyword to make this an error.

int y = 10;
someMethod(static x => x + y); // error!
const int y = 10;
someMethod(static x => x + y); // okay :-)

LDM history:

svick commented 4 years ago

@yugabe With static lambdas, I can specify which lambdas should be non-capturing and which are allowed to capture. How would that work with an analyzer?

yugabe commented 4 years ago

@svick if there was an analyzer, there was a rule (or rules) that would report on diagnostics. This analyzer would report on all lambdas in scope which were closing over anything. In my mind, this report would be disabled (or "silent") by default. You would have to opt-in to use this kind of diagnostic, either by flipping a switch in the IDE (I wouldn't prefer this, it would use discrepancy between team members) or using an .editorconfig file. By using an .editorconfig, you can scope where you would like the diagnostic reported as errors (or warnings, suggestions, etc.) in a folder-hierarchy. If you put it in your project root, it will report capturing lambdas as errors. Optimally you can suppress this message locally. Also, you can enhance the rule in any way, maybe by putting an [IgnoreClosure] or [EnforceClosure] attribute on the lambda (which is not yet supported though, AFAIK, but on the roadmap).

If going down this route, you can get compile-time checking in a controlled environment for closures without having to modify the language. The static keyword on an anonymous method/lambda instance is weird if you think about it. If you are not closing over anything, it won't matter whether you are putting or omitting the static keyword, the built assembly will be exactly the same. So it is just a compile-time check to ensure that you won't close over anything.

Making lambdas static would only induce a compile-time check to ensure nothing was closed over. I see it a lot easier to support this via an analyzer instead of a language feature. At least this is my understanding. If I'm wrong about this or I misunderstood something, please feel free to correct me, anyone.

CyrusNajmabadi commented 4 years ago

By using an .editorconfig, you can scope where you would like the diagnostic reported as errors (or warnings, suggestions, etc.) in a folder-hierarchy. If you put it in your project root, it will report capturing lambdas as errors. Optimally you can suppress this message locally.

But i don't want this. Because i'm not setting this rule for a scope. I'm choosing it for particular lambdas.

Also, you can enhance the rule in any way, maybe by putting an [IgnoreClosure] or [EnforceClosure] attribute on the lambda.

That's what static is. :)

If going down this route, you can get compile-time checking in a controlled environment for closures without having to modify the language.

You need a way to do attributes on lambdas with your proposal. That's a change to the language.

So it is just a compile-time check to ensure that you won't close over anything.

Yes. And we have precedence for that. For example we literally created static classes primarily for the use case of ensuring people didn't accidentally put instance methods in those classes. This was because the BCL had actually screwed up and done this in V1 and really wanted a simple way to annotate classes as not supporting that at all.

We also have the exactly analogous feature with static local functions, where static exists (and was added) solely for the case of preventing captures. This proposal just expands that out to lambdas, not just local functions. Indeed, that's how it's informally spec'ed at the moment. We would use the same logic and semantics as static-local-functions and apply them to lambdas if it had this modifier.

I see it a lot easier to support this via an analyzer instead of a language feature.

How would you write this as an analyzer today?

svick commented 4 years ago

@yugabe

If you put it in your project root, it will report capturing lambdas as errors.

That would cause a lot of noise for all the lambdas that do and should capture variables.

Optimally you can suppress this message locally.

If you're talking about something like #pragma warning disable 8422 /* code */ #pragma warning restore 8422, then I think that's way too verbose for something like this.

Also, you can enhance the rule in any way, maybe by putting an [IgnoreClosure] or [EnforceClosure] attribute on the lambda (which is not yet supported though, AFAIK, but on the roadmap).

AFAICT, no such thing is on the roadmap. "Permit attributes on local functions" (https://github.com/dotnet/csharplang/issues/1888) is coming, but that's a very different feature. The closest I can think of to this is "Attributes everywhere" (https://github.com/dotnet/csharplang/issues/2478), but that's not being pursued.

The static keyword on an anonymous method/lambda instance is weird if you think about it. If you are not closing over anything, it won't matter whether you are putting or omitting the static keyword, the built assembly will be exactly the same. So it is just a compile-time check to ensure that you won't close over anything.

Making lambdas static would only induce a compile-time check to ensure nothing was closed over. I see it a lot easier to support this via an analyzer instead of a language feature.

The same arguments would apply to static local functions, which are already supported.

iam3yal commented 4 years ago

@yugabe Right and I disagree with what you've said, analyzers doesn't solve all of the things, mainly two things that this feature guarantees and I'm not going to repeat what other people said but if it bothers you I can remove them.

p.s. Extension methods and/or analyzers are not silver bullets, some people think they are.

yugabe commented 4 years ago

@eyalsk no, it's all right, I just wanted to know if there was any specific reason.

P.S.: Same goes for language features, I suppose :)

@svick

If you put it in your project root, it will report capturing lambdas as errors.

That would cause a lot of noise for all the lambdas that do and should capture variables.

You can control .editorconfig in a folder hierarchy. I was on the assumption that some assemblies need this more than others, and in those you would like to enforce not capturing. If that is too broad, you can put it in a folder, and it will only apply to items in that folder.

By suppressing, I was thinking about the two approaches, yes, the #pragma warning disable and explicit attributes. I was looking for the attribute-related issues, but couldn't find them, so thank you for posting. I was under the impression that the #1888 would solve this, but it seems I was wrong.


Generally I am not against the feature as a whole, just that I don't find it clear when someone should use it and when someone shouldn't (application and/or library developers, mainly). I mean, of course, you want to avoid allocations, but that's something the framework and language should try to default to, maybe by not trying to allocate when passing method groups as delegate parameters, but I'm not sure that is possible; or even if it is, I don't know whether it would be a big breaking change or what other aspects of the language/framework it affects.

The other one is the intent of the developer. If the developer intends to capture variables, they do. If they "unintentionally" capture variables, why would they intentionally put the static keyword on the lambda? I don't think it makes much sense.

int y = 10;
someMethod(x => x + y); // captures 'y', causing unintended allocation.

with this proposal you could the static keyword to make this an error.

int y = 10;
someMethod(static x => x + y); // error!

Even using the static keyword has the problem I outlined above, it would not actually be a static method/variable. A related issue on the roslyn repo ([Proposal] Non-Capturing Lambdas with ==> #11620) had the ==> syntax for non-capturing lambdas, which could be better suited than the static keyword in this scenario, I think. But if method groups were not allocating when passed as delegate parameters, it would be easy to avoid allocation by creating a local static method.

yugabe commented 4 years ago

@CyrusNajmabadi sorry, I just saw your reply. I guess you are right, this is quite analogous to static local methods (and as I stated above, I was under the impression that attributes were coming to lambdas too). The only difference is when I declare a local method, there is a code refactoring that suggests I should make it static if I don't capture any local state. Here, the same would be the best, but then I would have a million messages saying I should make lambdas static, which I'd rather avoid.

The other question still unanswered: is there going to be a symmetric static local lambdas for Expressions too? Or this only relates to methods? If it only works on methods, it will be even more confusing to users (many people don't really know the difference between a lambda method and a lambda expression still).

svick commented 4 years ago

@yugabe

I was on the assumption that some assemblies need this more than others, and in those you would like to enforce not capturing. If that is too broad, you can put it in a folder, and it will only apply to items in that folder.

I think that you want this on a per-lambda basis. Anything above that is too broad.


maybe by not trying to allocate when passing method groups as delegate parameters

The issue about that is https://github.com/dotnet/roslyn/issues/5835, though it hasn't seen any activity in quite a while.


Even using the static keyword has the problem I outlined above, it would not actually be a static method/variable.

static is already used this way for static local functions. I don't think having a very different syntax for the same thing on lambdas makes sense.


The only difference is when I declare a local method, there is a code refactoring that suggests I should make it static if I don't capture any local state. Here, the same would be the best, but then I would have a million messages saying I should make lambdas static, which I'd rather avoid.

You can have a refactoring that doesn't produce any messages, so I don't think that's a reason against this feature.


is there going to be a symmetric static local lambdas for Expressions too?

The proposal says:

A static lambda can be used in an expression tree.

I think this means that that answer is yes.

yugabe commented 4 years ago

@svick thanks, that does it for me then, I'm on board.

foxesknow commented 3 years ago

I'd rather see the function being called decide that it doesn't want to be given a lambda that has captured anything.

For example, on ConcurrentDictionary we could define AddOrUpdate like this:

public TValue AddOrUpdate<TArg> 
(
    TKey key, 
    static Func<TKey,TArg,TValue> addValueFactory, 
    static Func<TKey,TValue,TArg,TValue> updateValueFactory, 
    TArg factoryArgument
);

Now there's no way the caller can accidentally capture the context and cause a memory allocation. They would need to pass any state they require in the the factoryArgument parameter.

// This will work
int value = 10;
dictionary.AddOrUpdate
(
    "Bob",
    (key, arg) => arg,
    (key, existing, arg) => existing + arg,
    value
);

// This will not work
int value = 10;
dictionary.AddOrUpdate
(
    "Bob",
    (key, arg) => arg,
    (key, existing, arg) => existing + value,  // NOT ALLOWED : value is captured
    value
)

This is particularly useful in high performance code where you may be queuing lambdas up to be processed on work queues. You don't want to have the caller accidentally do an allocation in order to create the lambda and the captured context, but you've got no way of enforcing it. Yes, someone can add static when declaring the lambda, but they can easily work around this by just removing the static

You could also allow the caller to specify static on the lambda, but I think the real power is in allowing the callee to indicate that it does not want to take a lambda that has captures the calling context.

CyrusNajmabadi commented 3 years ago

You don't want to have the caller accidentally do an allocation in order to create the lambda and the captured context, but you've got no way of enforcing it.

That's not true. This is exactly what analyzers are for. Writing one that checks that a particular api is only passed non allocating lambdas would be trivial.

foxesknow commented 3 years ago

You don't want to have the caller accidentally do an allocation in order to create the lambda and the captured context, but you've got no way of enforcing it.

That's not true. This is exactly what analyzers are for. Writing one that checks that a particular api is only passed non allocating lambdas would be trivial.

Are you seriously telling me that I've got a requirement to not allocate memory then I need to write an analyzer to enforce this? What's to stop someone disabling the analyzer?

Likewise, if the answer is to write an analyzer then why do we need the ability to declare the lambda as static in the first place? Surely you could just write an analyzer to check that the call to the particular api is only passed a non-allocating lambda as (apparently) that would be trivial to do...

NOTE: Writing an analyzer is not trivial!

333fred commented 3 years ago

Regardless of the merits of a different feature shape, this feature has already been implemented. If you have a new request, please open a new discussion.

CyrusNajmabadi commented 3 years ago

Are you seriously telling me that I've got a requirement to not allocate memory then I need to write an analyzer to enforce this?

Yes. I'm seriously telling you that.

Likewise, if the answer is to write an analyzer then why do we need the ability to declare the lambda as static in the first place?

Because there is no way to annotate a lambda to say that it shouldn't allocate. It's trivial to annotate a method to do that.

Surely you could just write an analyzer to check that the call to the particular api

Yes. That's what i said. static lambdas addresses hte other end. Where you have an API where you don't care if there is allocation or not, but you don't want a particular callsite to not allocate.

--

So, to sum up:

  1. static lambdas are for specific all site enforcement.
  2. analyzer is for API site enforcement.
foxesknow commented 3 years ago

static lambdas are for specific all site enforcement

No they're not, as you can just remove the static at the site to remove the enforcement! It's nothing more than an indication at the call site which not a requirement.

foxesknow commented 3 years ago

Regardless of the merits of a different feature shape, this feature has already been implemented. If you have a new request, please open a new discussion.

That's a fair point.

I would open a request, but it seems these days the people in charge of C# are more interested in adding pointless features to the language, such as:

333fred commented 3 years ago

I would open a request, but it seems these days the people in charge of C# are more interested in adding pointless features to the language

I mean, you're talking to 2 members of the ldm right now, so we're not going to agree with that assessment. It also does not change the fact that this feature, static lambdas, shipped at the beginning of the month. If you are interested in proposing a new feature, please open a new discussion.

CyrusNajmabadi commented 3 years ago

No they're not, as you can just remove the static at the site to remove the enforcement!

Yes. If you remove the thing enfocing something, it is no longer enforced. I can't help you there. The purpose of those was to be able to be explicit that you want callsite enforcement, and you'd then get that.

It's nothing more than an indication at the call site which not a requirement.

All indications are not requirements if you allow those indications to be removed.

jnm2 commented 3 years ago

I'd rather see the function being called decide that it doesn't want to be given a lambda that has captured anything.

I wonder if https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers would fit your use case. Beyond that, the language doesn't provide a facility for a method to restrict the set of things that callers can do in the lambda that is passed in the way that you're asking. This would be a separate proposal.

theunrepentantgeek commented 3 years ago

@foxesknow wrote:

I would open a request, but it seems these days the people in charge of C# are more interested in adding pointless features to the language ._.. _elided ...__

If you don't participate, you guarantee that your voice won't be heard. Open the issue, contribute to the discussion. Don't expect everyone to agree with you.

Luiz-Monad commented 3 years ago

@MgSam Too heavyweight for whom? people want to have control over what gets captured and sometimes how it gets captured and this locks the language into a specific behavior which seems too limited.

People can always have this as a syntactic sugar to capture lists but doing this first would be a shame.

I have a bad feeling about capture lists, they won't make it. 😆

As long as we are not getting that horrible C++ syntax for capturing locals with [x](){}, we are fine. The idea looks like a good idea, but the syntax is horrible. When I use C++ I end up mostly of the time never capturing anything, as its best for everything to be passed by value on lambdas, it stays more true to the meaning of a lambda in functional programming. Or when you need to capture something, having to pay attention anyway to what the lambda does, so you go back and put all the variables to be captured in the capture-list, you may as well break the lambda if you have too many variables, or use a struct and pass its entire content as a single var. So I end up doing either []() {} or [=](){} most of the time, as I also use a lot of auto, I end up don't bothering with specifying each and every single variable, and that's precisely how static will work in C# when you omit it, it will capture all it can capture in a single giant heap object, you either want to capture anything or nothing, specifying every single variable is too heavy. How many variables do you even have in your scope ? perhaps too much (that's usually a problem with C++ programs, but C# code usually have more small methods with less variables in the scope, but that's anecdotal) .

Also, as someone who extensively uses F# too, I like the idea of static, most lambdas should be pure functions and not capture anything from the scope to the closure. If you want to capture variable, you just don't put static on it. And there's this thing called curry/uncurry, if you really need to capture some locals, but not others, you do something like this:

people want to have control over what gets captured and sometimes how it gets captured and this locks the language into a specific behavior which seems too limited.

const int y = 10;
someMethod( static x => x + y );  // okay :-)
int z = 9;
someMethod( x => (static (w, a) => w + a + y)(x, z) ); // also okay, and captures the z only, we don't really need capture lists.
//it could even be
someMethod( (const x) => (static w => w + x + y)(z) );  // with this pattern you can precisely say what you want to capture
int k = 8;
someMethod( (const x) => (static (k, z) => k + z + x + y)(k, z) );   //even better, hide outer scope to avoid misuse

If you are already paying the cost of a heap allocation, you may as well pay an extra call. And that's not even needed, as the compiler could (in theory) easily inline (static (w, a) => w + a + y as the Invoke method in the DisplayClass, I'm pretty sure the F# compiler does that extensively for performance reasons.