Replies: 4 comments
-
This would, presumably, also apply to 'static delegates': #302 |
Beta Was this translation helpful? Give feedback.
-
I've been struggling with the limitation of Func and Action not allowing parameter names and would love to see a fix for that. I've started doing a lot more functional-style programming where I pass functions into constructors and the lack of parameter names makes it very unclear what type of function I'm expecting. So I started creating custom delegate types and at first I thought "hey this is a really cool unused c# feature". But the novelty wore off pretty quick; each time I have to do that it feels cumbersome. Sometimes I need to define a delegate even though it is only used in one place. It is tough to figure out good names for them too. For example, suppose I have a few flavors of delegates for editing a Person record, as shown below.
One workaround I tried was using Action and Func but instead of having lots of parameters I use a single tuple parameter with names. That is kind of an improvement but the extra parenthesis and need to pull out the component tuple parts is awkward. I don't really want these parameters wrapped inside a tuple just so I can get good naming support. I also don't trust tuple component names like I trust method argument names because they are kind-of optional.
Finally I too get confused looking at the intellisense for Linq and other methods. |
Beta Was this translation helpful? Give feedback.
-
You are far from alone with this. Not sure how it would work, but your idea of being able to create a parameter to a method like |
Beta Was this translation helpful? Give feedback.
-
@gafter I think the issue is that C# language provides only syntax for Multicast Delegates |
Beta Was this translation helpful? Give feedback.
-
@axel-habermaier commented on Sun Apr 03 2016
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.@HaloFour commented on Sun Apr 03 2016
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
<>M_delegate
would certainly not be suitable.The only real "problem" that this proposal seems to try to address is that the argument names of the general-purpose
Action
andFunc
delegates are general-purpose and not descriptive. But the solution for trying to address this through some combination of autogenerated types and/or attributes seems more complex than simply just defining a new delegate type.@svick commented on Sun Apr 03 2016
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
(*)
part, but I'm still not sure it's the right way to go. For example, the delegate type for theCreatePredicate
method in the example would bebool(T)(T)
, that doesn't strike me as easy to understand, even when compared withFunc<T, Func<T, bool>>
And I believe the syntax is ambiguous, at least assuming your proposed delegate types would syntactically behave like any other type:
Action().Combine();
could mean either "invokeAction
(which could be for example a property) and then callCombine()
on the result" or "callCombine()
on the delegate typeAction()
".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
var
.@axel-habermaier commented on Sun Apr 03 2016
@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
Aggregate
clearly shows that its not just about "simply" defining a new delegate type. Also, this proposal not only considers missing parameter names and IntelliSense deficiencies a problem, but also the non-unified declaration ofAction
andFunc
-based delegates, which are sometimes even impossible to use altogether.@svick:
bool(T)(T)
actually reads quite nice in my opinion, because that's exactly what is does. Maybe it should be possible to write(bool(T))(T)
as well? Not sure if that makes it clearer, though. Also, nested functions always mess up signatures, regardless of the syntax that you use.@svick: Regarding the ambiguity of
Action().Combine()
: Good catch! The grammar would be ambiguous because the parser could not decide whetherAction()
represents an invocation expression or a type expression.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 (
out
andparams
) could be encoded in attributes as well. I still think that this proposal has value, even if generated delegate types could only be used internally within an assembly.@HaloFour commented on Sun Apr 03 2016
@axel-habermaier It doesn't solve that non-unification, it just uses a different syntax to describe it.
int(int)
andvoid(int)
would still remain incompatible delegate types.Action
andFunc
weren't added to the BCL to address any language limitations or to attempt to handle every possible delegate signature that could ever be used. Simply declare a new delegate type. Then you'd have a named type that you can reuse anywhere you want, which I think is infinitely nicer than trying to force function signatures into the middle of other function signatures.@axel-habermaier commented on Sun Apr 03 2016
@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
int(int)
andvoid(int)
are not signature-compatible is due to their very nature; yet the syntactic declaration would be the same. The difference betweenAction
andFunc
solely exists because of a C#/CLR limitation; F# doesn't have that problem (it usesunit
forvoid
, which is a real type), and C++ allowsvoid
as template arguments. What other reasons could there possibly be for separatingAction
andFunc
?@DavidArno commented on Mon Apr 04 2016
Being able to have parameter names added to
Func
andAction
, much like the tuple proposals will add names toTuple<...>
, is a nice idea. However, this almost seems a secondary aspect of what you are proposing. The main proposal seems to be for lots of new syntax just to avoid creating custom delegates for edge-cases whenout
,ref
or pointers can't be avaoided.@vladd commented on Mon Apr 04 2016
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.
@svick commented on Mon Apr 04 2016
@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:
@orthoxerox commented on Mon Apr 04 2016
@svick what would be the type of
signal
, then?void(int)(int, void(int))
? Well, it's better than the spiral rule, but unlike function invocation it's right-associative. I don't know how much this will complicate the parser.(int, int->void)->int->void
is in my opinion easier to parse for both humans and machines.@DiryBoy commented on Tue Apr 05 2016
I think
(void(object))(Xyz)
is ambiguous whenXyz
is unknown? IfXyz
is a type, then this means a delegate type that returns a delegate, otherwise it's a cast?@svick commented on Tue Apr 05 2016
@orthoxerox The type of delegate to
signal
, yes.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.
@axel-habermaier commented on Wed Apr 06 2016
How about explicit naming was allowed for the generated delegate types to solve the problem of unutterable, possibly changing public names? Such as
The compiler could require all externally visible, auto-generated delegates to be explicitly named.
@GeirGrusom commented on Wed Apr 06 2016
@axel-habermaier
void
also is a real type though. It just isn't handle as well by either C# or the runtime.Calling
typeof(Func<>).MakeGenericType(typeof(void))
throws an ArgumentException.@axel-habermaier commented on Wed Apr 06 2016
@GeirGrusom: True. Interestingly, you can even instantiate
void
like so:System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(void))
😄Beta Was this translation helpful? Give feedback.
All reactions