Open LPeter1997 opened 1 year ago
Few notes:
FSharpFunc
is an abstract class. In C# myList.Select(x => x)
will create static function and pass it into Func<T, T>
. While in F# myList |> Seq.map (fun x -> x)
will create sealed class inherited from FSharpFunc<T, T>
. To my knowledge classes with virtual methods work faster than delegates. Also FSharpFunc
is built with mind on currying and optimizes some partial application scenarios.
What type of swapper
would be if it stored inside public field? If name would be synthesized it should be reliable, so downstream libraries won't break if upstream is just recompiled.
Java have so called functional interfaces which are equivalent of delegates in JVM world. These are just simple interfaces, that compiler threat differently. If feature like this is to be implemented, it's better to use object expressions which allow to implement arbitrary interface/abstract_class
So FSharpFunc
for us is in the same boat as Func
, it can't escape the limitations of generics. I wonder if we should also model delegates as virtual functions and provide a conversion to the C# delegates for interop or something. I guess we need the input of some lowlevel/perf peeps here.
What type of
swapper
would be if it stored inside public field? If name would be synthesized it should be reliable, so downstream libraries won't break if upstream is just recompiled.
Indeed, this is a hard problem and stability came up as a problem. I can not answer that, but can bring more problematic code that has been brought up, for example a C# method taking SomeDelegate[]
, in which case we'd have to do some funky variance with arrays that I'd rather avoid. Since there are an infinite number of edge-cases, in the end we'll likely have to introduce delegate type declaration anyway.
I didn't know object expressions were a thing. At first glance they look like anonymous classes from Java.
I'd like to lay out the current situation of delegate types in C#, try to give a bit of (likely incorrect) history and tendencies and propose a direction for Draco.
What are delegates?
In essence, they are the mechanism to describe the types of methods/functions. This allows us to take functions as parameters or return functions from other functions. Likely every .NET developer is familiar with passing in functions as parameters, as it's the heart of LINQ. When we can view functions just like any other values, we call them first-class functions. Some very simple examples:
Delegate types in C
The first form of delegates came in C# 1.0 and they are still needed today in very rare scenarios. You simply declare a method signature with the keyword
delegate
, and specify the delegate type name where the method name would come:Importantly, delegate types are always considered completely different types, even if they match the signature in every aspect. They can only be explicitly converted to one another:
This means that C# experimented with nominal-typing for methods, trying to attach meaning behind the signature shape. While nominal typing can be great for types with more interesting behavior, it did not really work out on method level, and around C# 3.0 two delegate types were shipped with .NET that we use to this day:
Func<...>
andAction
.There is nothing magical with
Func
andAction
, they are a bunch of delegate declarations within the BCL (see Func and Action). While this does not eliminate the flaws of nominal typing, as long as everyone uses these declarations, methods with identical signature should be compatible with each other.There is one thing that these declarations can not fix: since generics can't describe byref parameters and the like, for these methods we still need to declare an old-school delegate type. There is also the annoying differentiation between
Func
andAction
, becausevoid
can not be a generic argument.Delegate-types in F
I'm not too familiar with F# personally, but as far as I know, they have the exact same toolset as C#. They implicitly type their lambdas using
FSharpFunc
, which is roughly equivalent toFunc
.Proposal for Draco
It's very clear to me that nominal typing didn't work out for methods (see
Func<T, bool>
being used in so-so many places of the BCL instead ofPredicate<T>
). While the fundamental decision of delegate types can not be removed, we can hide it from the user. I propose a structural notation for our function types:Functions will always be implicitly typed with their exact signature type, so the above works even without the type annotations:
Low-level details
Of course, the question is how this is supposed to work on a codegen level.
For the majority of cases, we can use
Func
andAction
ourselves. If there is nothing special in the signature and these types can describe it, we should default to these. When the function returns unit, we useAction
, otherwise we useFunc
. This will greatly help interop with C#.For cases where these are insufficient, we need to generate a delegate type, in case the signature needs to appear somewhere. For example, the following code:
Would be equivalent to the following C# code (only the delegate and main functions):
Whenever during codegen delegates of different types are assigned, we implicitly call the converting constructor that one would explicitly call in C#.
Long-term suggestions
One might argue that nominally typed methods do come in handy sometimes. I believe that we could take inspiration from Java here, namely that we could allow single lambdas to implement single-method interfaces. Not only could this be a nice utility, but it would fill in the gap for the very few cases where nominally typed functions - mainly for behavioral patterns - are preferable.