-
Notifications
You must be signed in to change notification settings - Fork 4k
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
[Proposal] First Class Delegate Types #10303
Comments
I don't understand this statement about C# not having syntactic support for delegates as a first-class citizen. C# absolutely does have syntactic support, which is why you're not required to define a delegate as a class complete with all of its required members, which is what you must do in IL. Considering that a CLR delegate is a proper named type, C#'s syntax for defining a delegate is about as succinct as you can get. Autogenerated names as a part of a public contract is a very fragile solution. If the developer inadvertently does something which causes the generated name to change then all consumers of that delegate will break. The names also have to be CLS-compliant in order to be usable from other languages, so something like The only real "problem" that this proposal seems to try to address is that the argument names of the general-purpose |
C function pointer syntax is notoriously hard to understand, especially in more complicated cases. Your proposal makes it better, since you're removing the middle And I believe the syntax is ambiguous, at least assuming your proposed delegate types would syntactically behave like any other type: Also, currently, unutterable names are only used as implementation details, but you are proposing to expose them publicly. I think that is not acceptable, for several reasons: it's not CLS compatible, it means the unutterable name can never change, it makes the code hard to use for languages without |
@HaloFour: Regarding "first class": What I meant is that C# has no first class support for delegate type expressions. It does indeed have first class support for delegate declarations. @HaloFour: The fact that no new delegate type was defined for @svick: @svick: Regarding the ambiguity of Regarding generated delegate types: This is indeed unfortunate. The problem could only be avoided if a) the CLR allowed generic pointer parameters and b) the CLR allowed ref/out generic parameters. The remaining information ( |
@axel-habermaier It doesn't solve that non-unification, it just uses a different syntax to describe it. |
@HaloFour: The problem is that declaring delegate types doesn't scale especially with more and more functional programming coming into C#. Hence, no one does it, not even the BCL. In fact, the framework design guidelines event suggest to avoid custom delegate types. The fact that |
Being able to have parameter names added to |
Please please please don't introduce the abomination known as "C function naming scheme" into such a nice, modern, readable and understandable language as C# is. The spiral rule is very unnatural and counter-intuitive thing. A syntax that requires deobfuscation cannot possibly be a good syntax. |
@vladd If I understand this proposal and the C syntax correctly (at least to some degree), then I think the spiral rule would not apply. If you look at the example you linked to:
Then I think the equivalent using this proposal would be:
|
@svick what would be the type of |
I think |
@orthoxerox The type of delegate to I originally thought this was a terrible syntax (because of the C heritage). Now I'm not sure, but the ambiguity issues might be a deal breaker. |
How about explicit naming was allowed for the generated delegate types to solve the problem of unutterable, possibly changing public names? Such as class X
{
// using a special attribute
public static void M([DelegateName("MyDelegate")] bool(int* i) x) { ... }
// inline name declaration
public static void M(bool MyDelegate(int* i) x) { ... }
} The compiler could require all externally visible, auto-generated delegates to be explicitly named. |
Calling |
@GeirGrusom: True. Interestingly, you can even instantiate |
Issue moved to dotnet/csharplang #470 via ZenHub |
With more and more functional language features coming to C#, I think it is time to make delegate types first class citizens in C# to improve readability and tooling. The following proposal does that: It discusses the issues that I think C# currently has with delegate types and proposes a new syntax to solve these issues in a way that is mostly fully transparent; i.e., it does not change the generated code in most cases compared to what you can do in C# 6.
Note that this proposal exclusively focuses on delegate types as opposed to creating and instantiating delegate instances. For the latter, we have automatic method group to delegate conversions, anonymous delegates, and lambda functions. All of these features are completely orthogonal to this proposal and would continue to work unmodified.
Current Approach
C# currently has no first class syntactic support for delegate types. Instead, it treats delegates mostly like classes, which they indeed are under the hood. The idea was to require all delegate types to be explicitly declared and named for semantic reasons in order to convey meaning to the user: For example, the
System.Collections.Generic.List<T>
class has aFindAll
method taking aPredicate
delegate, defined as follows:In later versions of C# and .NET, it became somewhat less common to declare custom delegate types for "each" method. This is probably mostly due to the high number of types one has to introduce with little benefit; also, the idea was that the name conveys meaning, but since coming up with names is hard, they were mostly pretty generic anyway (for events, for instance, the names are often of the form "EventName + Handler suffix", which doesn't really tell you anything). In particular, you have to F12 into the delegate declaration to actually see the parameters the delegate expects.
Things both got worse and improved when the families of
System.Action
,System.Func
, andSystem.EventHandler
delegate types where added to the base class library. Most new types and methods throughout .NET make use of these delegate types today. In particular, LINQ'sWhere
method does not usePredicate
likeList<T>.FindAll
, instead it usesFunc<T, bool>
.Problems with the Current Approach
Delegates are a successful feature of both C# and the CLR. However, the way C# currently handles delegate types has several problems that are outlined in the following.
Side Note: Interestingly, F# does not use delegates to represent its function values but uses
FSharpFunc
-derived types instead. The reason seems to be to efficiently support function currying, which I'm not proposing here. Hence, C# would continue to use the CLR's special support for delegates.Unhelpful tooling
The tooling is problematic (read unhelpful) when using LINQ's
Aggregate
method, for instance, which is declared as follows:In particular, the tooling experience is completely broken when I try to use the method as follows:
Question: What am I doubling, the accumulated value or the elements in the list? I for one never know and have to look it up in the documentation in the remarks section (!!); the first argument (
a
in this case) is the aggregated value by the way. There is no way within Visual Studio (that I am aware of) to figure that out. SinceAggregate
usesSystem.Func
, it doesn't even help to F12 into the delegate declaration ofSystem.Func<T1,T2>
, as the delegate arguments just like their types have generic namesarg1
andarg2
, which is not helpful at all.Limitations of Action and Func
Another annoyance is the need for both
System.Action
andSystem.Func
. Asvoid
is not a real type, it isn't allowed as a generic argument, hence you can't simply writeFunc<int, void>
for a void-returning function taking an integer. Hence,Action<int>
was introduced, which always seemed somewhat of a hack to me.Because of their use of generics,
System.Action
andSystem.Func
have other limitations: You can't haveref
andout
parameters,params
parameters, or make use of pointer types, for instance. In all of these cases, you have to fall back to declaring your own delegate type explicitly.In short,
Action
andFunc
are, in my opinion, an imperfect library-based solution to work around a C# language limitation.Proposed Solution
The proposed solution is simple: Make delegate types a first class syntactic construct, similar to how tuples apparently become first class in C# 7. There are several issues to discuss:
Spoiler: While there will always be use cases for explicitly declared custom delegate types even if it was decided to implement this proposal, you would most likely never explicitly use
Func
orAction
again, and ref/out/params parameters or pointers would also no longer be a reason for explicitly declared delegate types.Syntax
The proposal is to add a new type expression syntax similar to how tuple types work.
Other Languages
Let's first take a brief look at some other languages to get some ideas and inspiration of how delegates (or more generally: function pointers) could be syntactically represented. For instance, let's declare a function pointer/delegate type for a function returning a
bool
and taking anint
:In C:
C++ uses the same syntax as C for free-standing functions and a slightly different syntax for member functions. The
std::function
template introduced with C++11 unifies both syntaxes into something that I personally find quite nice:Functional languages like F# of course have first class support for function types:
int -> bool
Proposed syntax
I would propose to use a C-like syntax for all delegate types in C#, as the F# syntax just doesn't feel very C#-ish. Disclaimer: I'm not really sure whether the syntax would introduce ambiguities; if so, we could certainly come up with another acceptable syntax that is unambiguous. Therefore, this syntax is just a suggestion; please don't focus on it too much. Some examples:
Ideally, we would also be able to specify parameter names to improve tooling just like we can do for
delegate
declarations today; names would be optional, however. For instance:Examples
Here are some small code examples making use of the new delegate type syntax:
Code generation
The idea is that the new syntax is only syntactic sugar for
Func
andAction
where possible. Only when these types are not available, for example because the compilation target is an old version of .NET, or because the delegate type uses pointers, ref/out parameters, etc., is a new delegate type introduced. Examples:Using Action and Func
If parameters are named, I suggest to add a
NamesAttribute
(or something similar, probably unified with the attribute used to encode tuple names) to the base class library that can be used to encode parameter names which are subsequently picked up by IntelliSense. That would probably work in a very similar way to how names are encoded for tuples. Examples:Open question
How would we encode the names of the elements in the tuple returned by the delegate returned by a method as in the following case? That could become complicated, but a solution to this problem probably already exists for tuples; after all, they can also be nested.
What about ref/out/params/pointers?
That is more complicated; as
Action
andFunc
cannot be used in these cases, the compiler would have to declare a new delegate type. This, however, has the same type unification issues that have previously been discussed for tuples, though I don't think there is a generic solution for this problem that is both efficient and doesn't require CLR support. Anyway, let's consider doing the simplest thing that could possibly work: Just declare a delegate type in the containing namespace with appropriate accessibility. For instance:One could of course consider unifications of the generated delegate types within an assembly, for instance when two methods declare a parameter with the same delegate syntax. There is one problem though that is illustrated by the following code:
That is unfortunate. For tuples, this problem can be avoided by adding the underlying tuple type to the base class library. This is not possible however for the delegate types we're trying to declare here; after all, there already are
Func
andAction
in the base class libraries, but we cannot use them due to their generic nature (by the way, do tuples support pointers? Probably not.)I see two potential solutions for this problem: Either fix the CLR to allow structurally equal delegate types to be efficiently converted into each other. Or wrap the delegate in another delegate instance, which of course is inefficient because that would result in two delegate instantiations and invocations instead of just one. That is:
Given that
Action
andFunc
based delegates are the common case, the performance penalty of the above might be acceptable. Once the CLR provides an efficient means to convert delegates, the generated code could transparently make use of that after a recompile for newer .NET versions, such as:Summary
Would this proposal solve the problems mentioned above?
Yes: If LINQ's
Aggregate
function would be redefined as above, IntelliSense could show the parameter names, solving the usability issue. While we're at it, tooling could also show parameter names for custom declared delegate types. Note thatAggregate
can be changed without breaking backwards compatibility: The compiled method will be unchanged (both the implementation as well as the execution-relevant metadata); only the[ParameterNames]
attribute will be added, which only affects tooling, but doesn't affect binary compatibility.Yes: We would not ever write
Action
orFunc
again; instead, the compiler would pick the correct type automatically.Yes: We could use ref/out/params parameters and pointers with the same syntax.
Yes: Other languages and previous versions of the compiler would be unaffected by this change. They would simply see the
Action
andFunc
types as before, not getting any of the tooling improvements, however. Again, replacing oldAction
andFunc
declarations with the new syntax is binary compatible.No: There is a type unification problem for non-
Action
orFunc
based delegates between different assemblies.No: Other languages and older compilers would see compiler-generated delegate types in certain cases, though not in the common cases using
Action
andFunc
.Could a code fix automatically convert code to the new syntax?
Yes: All references to
Action
orFunc
could be replaced with the new syntax without affecting the semantics of the program. In fact, the exact same code would be generated as before. Manual intervention would be required, however, to come up with meaningful parameter names, if desired. Custom declared delegate types such asPredicate<T>
would of course remain completely unaffected by this proposal and where they make sense, continue to be fully supported.The text was updated successfully, but these errors were encountered: