Request: a solution that allows us to not have to write 6500 different delegates & overloads #2668
Replies: 67 comments 3 replies
-
What exactly do you propose? The CLR and C# don't know anything about lambdas, they only know about delegates, and the CLR requires separate nominal types per signature. Those types all have the common Also, this proposal seems insanely narrow, in that it only applies to a specific API for a specific runtime that requires IL rewriting to function at all. I'd have to think that there are more general-purpose ways to build this based on what exists in the language. |
Beta Was this translation helpful? Give feedback.
-
we don't need a delegate. We just need some type that we're allowed to assign any lambda expression to, and can from IL reason about where the compiler generated function is that was made for the lambda. |
Beta Was this translation helpful? Give feedback.
-
All lambdas compile to delegates, even in the case of expression trees. How would such a language feature apply to projects outside of this specific use case? Why does it have to use lambdas? This feels like using lambda syntax to accomplish pattern matching instead of using patterns. |
Beta Was this translation helpful? Give feedback.
-
How about something like this: .WithCode((Position ref_position, Velocity in_velocity) =>
{
ref_position.Value += in_velocity.Value;
}); Your build system would throw if the user didn't properly name things. It could then transform the above however it wanted (making actual 'ref' parameters for Now you'd just need the 8 overloads for the 8 types of actions you would take. You'd still get compile time checking since your tool would verify the 'name' specified how the parameter needed to be passed. Basically, if you have an IL processing step, you can basically encode things however you want. Just use a pattern that your processing step will look for (and fail on if incorrect), and then rewrite to whatever actual form your "jobsystem" wants. |
Beta Was this translation helpful? Give feedback.
-
There are some restrictions for C#. You can't mark a generic attribute as 'digitable', you can't mark an operator as 'commutativity', you can't overload function by the count of generic arguments without copypasting. I don't know your business flow or demands, but when I face with stuff like this that means that I should achieve my goal in another way. |
Beta Was this translation helpful? Give feedback.
-
class DynamicSignatureDelegate
{
public DynamicSignatureDelegate(object target, MethodInfo methodInfo){}
} if such class is used instead of a Delegate, compiler should emit
instead of
Being able to convert an arbitrary lambda to a MethodInfo would be also quite useful in non-Unity scenarios. This could also play nicely with memberof proposal: Foo(memberof int (int x, int y) => x+y); |
Beta Was this translation helpful? Give feedback.
-
You can also define your overloads following such a convention that DOTS looks like an engine that's already in the wild. How much does your workflow depend on a feature like this being adopted? |
Beta Was this translation helpful? Give feedback.
-
Is this actually a problem? I assume you're just generating them. And presumably your IL shaker will just remove teh ones that aren't used (of even remove all of them since you don't actually use delegates at the end of the day). So is there an actual problem that would be solved by a language feature here? |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi the problem in the OP is "It makes visualstudio slow, resharper slow, horrible for docs, etc" It's also insanely confusing for users who have no idea what all this stuff is and will be given a pile of weirdly acronym'd names in their autocomplete. Anyway from a perf perspective, very few .net tools are expecting a type to have >100 of anything and having 6500 is a good way to discover all the n^2 algorithms that were invisible with low member counts. Oh and regarding ordering of in/ref etc. to reduce overloads, we've considered this and it's not too bad. It does come with the significant cost of confusing compiler error. Someone changes a ref to an in or vice versa, and then the compiler gives a weird message about some acronym and param types not matching, and what the user is supposed to do is rearrange their parameter list. Not great UX. What we actually want to do is let people pass in a lambda with whatever they want (lambda because the syntax is so close to an ordinary |
Beta Was this translation helpful? Give feedback.
-
Something like this to work? public void WithCode<TAction>(TAction action)
where TAction : Delegate
{
// ...
} Though currently that will give the error
Specifying them then returns you back to the current situation (lots of delegates); also poor usability in the generic type: WithCode<RR<Position, Velocity>>((ref Position position, in Velocity velocity) =>
{
position.Value += velocity.Value;
}); For WithCode((ref Position position, in Velocity velocity) =>
{
position.Value += velocity.Value;
}); Also concrete doesn't work public void WithCode(Delegate action)
{
} As it gives
|
Beta Was this translation helpful? Give feedback.
-
Here is an idea, it make thing much shorter but it has its drawbacks : public delegate void WithCodeHandler<RefParameters, IntParameters>(ref RefParameters referenceParameters, in IntParameters inParameters);
public struct ParameterSet<T0> {
public T0 t0;
}
public struct ParameterSet<T0, T1> {
public T0 t0;
public T1 t1;
}
public struct ParameterSet<T0, T1, T2> {
public T0 t0;
public T1 t1;
public T2 t2;
}
public struct ParameterSet<T0, T1, T2, T3> {
public T0 t0;
public T1 t1;
public T2 t2;
public T3 t3;
}
public void WithCode<T0, T1>(WithCodeHandler<ParameterSet<T0>, ParameterSet<T1>> function) { }
public void WithCode<T0, T1, T2>(WithCodeHandler<ParameterSet<T0>, ParameterSet<T1, T2>> function) { }
public void WithCode<T0, T1, T2>(WithCodeHandler<ParameterSet<T0, T1>, ParameterSet<T2>> function) { }
public void WithCode<T0, T1, T2, T3>(WithCodeHandler<ParameterSet<T0>, ParameterSet<T1, T2, T3>> function) { }
public void WithCode<T0, T1, T2, T3>(WithCodeHandler<ParameterSet<T0, T1>, ParameterSet<T2, T3>> function) { }
public void WithCode<T0, T1, T2, T3>(WithCodeHandler<ParameterSet<T0, T1, T2>, ParameterSet<T3>> function) { }
public void WithCode<T0, T1, T2, T3, T4>(WithCodeHandler<ParameterSet<T0>, ParameterSet<T1, T2, T3, T4>> function) { }
public void WithCode<T0, T1, T2, T3, T4>(WithCodeHandler<ParameterSet<T0, T1>, ParameterSet<T2, T3, T4>> function) { }
public void WithCode<T0, T1, T2, T3, T4>(WithCodeHandler<ParameterSet<T0, T1, T2>, ParameterSet<T3, T4>> function) { }
public void WithCode<T0, T1, T2, T3, T4>(WithCodeHandler<ParameterSet<T0, T1, T2, T3>, ParameterSet<T4>> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5>(WithCodeHandler<ParameterSet<T0, T1>, ParameterSet<T2, T3, T4, T5>> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5>(WithCodeHandler<ParameterSet<T0, T1, T2>, ParameterSet<T3, T4, T5>> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5>(WithCodeHandler<ParameterSet<T0, T1, T2, T3>, ParameterSet<T4, T5>> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5, T6>(WithCodeHandler<ParameterSet<T0, T1, T2>, ParameterSet<T3, T4, T5, T6>> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5, T6>(WithCodeHandler<ParameterSet<T0, T1, T2, T3>, ParameterSet<T4, T5, T6>> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5, T6, T7>(WithCodeHandler<ParameterSet<T0, T1, T2, T3>, ParameterSet<T4, T5, T6, T7>> function) { } And it would be used like this : .WithCode((
ref ParameterSet<Position, Rotation> refParameters,
in ParameterSet<Velocity> inParameters) => {
refParameters.t0.Value += inParameters.t0.Value;
return;
}); Notice I haven't put the value type, only ref and in. Is it required ? That's already a lot less combinations You could also let the user make his own Parameter containers, that would be recognized by your build system thanks to an attribute or by inheriting from a specific abstract type ( |
Beta Was this translation helpful? Give feedback.
-
@benaadams how about |
Beta Was this translation helpful? Give feedback.
-
WithCode(delegate(ref Position position, in Velocity velocity)
{
position.Value += velocity.Value;
}); Gives
|
Beta Was this translation helpful? Give feedback.
-
@ogxd cool idea, though "t0" etc. are hard to read. It can be improved just a little.. .WithCode((ref (Position pos, Rotation rot) refs, in (Velocity vel) ins) =>
{
refs.pos.Value += ins.vel.Value;
}); ..via receiving ValueTuples as ref/in. Unfortunately there's no way to do a single-element value tuple (#883). :( |
Beta Was this translation helpful? Give feedback.
-
@scottbilas that is a great usecase for tuples ! It even makes the overloads much cooler :) For single-element values, I'd say that not making a tuple is fine. The user would have to know that he should use parenthesis to deal with multiple That would look like this : public delegate void WithCodeHandler<Refs, Ins>(ref Refs refs, in Ins ins);
public void WithCode<T0, T1>(WithCodeHandler<T0, T1> function) { }
public void WithCode<T0, T1, T2>(WithCodeHandler<T0, (T1, T2)> function) { }
public void WithCode<T0, T1, T2>(WithCodeHandler<(T0, T1), T2> function) { }
public void WithCode<T0, T1, T2, T3>(WithCodeHandler<T0, (T1, T2, T3)> function) { }
public void WithCode<T0, T1, T2, T3>(WithCodeHandler<(T0, T1), (T2, T3)> function) { }
public void WithCode<T0, T1, T2, T3>(WithCodeHandler<(T0, T1, T2), T3> function) { }
public void WithCode<T0, T1, T2, T3, T4>(WithCodeHandler<T0, (T1, T2, T3, T4)> function) { }
public void WithCode<T0, T1, T2, T3, T4>(WithCodeHandler<(T0, T1), (T2, T3, T4)> function) { }
public void WithCode<T0, T1, T2, T3, T4>(WithCodeHandler<(T0, T1, T2), (T3, T4)> function) { }
public void WithCode<T0, T1, T2, T3, T4>(WithCodeHandler<(T0, T1, T2, T3), T4> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5>(WithCodeHandler<(T0, T1), (T2, T3, T4, T5)> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5>(WithCodeHandler<(T0, T1, T2), (T3, T4, T5)> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5>(WithCodeHandler<(T0, T1, T2, T3), (T4, T5)> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5, T6>(WithCodeHandler<(T0, T1, T2), (T3, T4, T5, T6)> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5, T6>(WithCodeHandler<(T0, T1, T2, T3), (T4, T5, T6)> function) { }
public void WithCode<T0, T1, T2, T3, T4, T5, T6, T7>(WithCodeHandler<(T0, T1, T2, T3), (T4, T5, T6, T7)> function) { } .WithCode((ref Position position, in Velocity velocity) => {
position.Value += velocity.Value;
});
.WithCode((ref (Position position, Rotation rotation) refs, in Velocity velocity) => {
refs.position.Value += velocity.Value;
refs.rotation.Value.y += 0.1f;
});
.WithCode((ref Position position, in (Velocity velocity, Rotation rotation) ins) => {
position.Value += Vector3.Scale(ins.velocity.Value, ins.rotation.Value);
}); |
Beta Was this translation helpful? Give feedback.
-
Currently, i see these options on the table:
Is that a fairly accurate lay of the land? |
Beta Was this translation helpful? Give feedback.
-
Thanks for all the input from everyone. The comment from @kekekeks is exactly what I'm hoping for. #2668 (comment) There's a wide range of alternative options kicked around (much appriciated). Most of them I find not particularly appealing:
One "oh almost so close" scenario is limiting the amount of delegate combinations through restrictions of the "ref arguments always have to come before in arguments" kind. I think it is actually fine to require this, except that I cannot find a way to give the user a great error message when they get it wrong (which they will all the time). If I could find a way to have a overload resolution failure because the delegate the user needs doesn't exist result in an error I can write that clearly explains what the problem is, and how they need to fix it (ideally with an analyzer fixup), then I think we'd totally go for that approach. |
Beta Was this translation helpful? Give feedback.
-
You could always have an analyzer that provides this error message. You will likely end up with two error messages for the issue (one from Roslyn and one that you provide) and that will likely take some user education.
I don't believe this is accurate. I think the compiler has a bunch of heuristics about picking which overload it thinks is best in error scenairos. If we can well define your error scenarios we could potentially tweak the compiler heuristic such that it did things better here if it would not regress other areas. |
Beta Was this translation helpful? Give feedback.
-
(I just want to point out that there are 3279 delegates in the file you linked, not 6500 - the rest of the file is method signatures. That's either a good thing or a bad thing depending on your point of view!) |
Beta Was this translation helpful? Give feedback.
-
Part of the combinatorial explosion comes from the fact that you've got Do you need both If you can restrict these delegates to just |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Of course, I'm aware of this. Reading the thread, there's no indication that the OP wants to use I don't know what happens to that delegate once it's been rewritten. It could be that it's rewritten such that it's e.g. a method on a class, and the different components (e.g. It could also be that the only parameters to this delegate are components, and it could be that components are always structs which you would never want to pass by value. After all, if your delegate took a primitive like Another possibility is that It could well be that OP has a good reason for wanting both plain parameters and |
Beta Was this translation helpful? Give feedback.
-
Can enforcing a function (as opposed to a lambda) and a simple nameof() be a solution here?
This seems pretty clean to me. You could potentially even rewrite the IL to follow this pattern if you're worried about people being confused and using a hard-coded "Handle_Position_Velocity". With this restriction you can even mark-up your functions (HandlePositionVelocity) with attributes in the future to hint at optimizations. |
Beta Was this translation helpful? Give feedback.
-
@canton7 I can explain why we want n^3 combos rather than n^2
We need to know programmer intent via I would consider anything we come up with for reducing # combos to be a substandard solution. It will always come with a UX price. Also, if we provide n of something, a user will inevitably want n+1 of those, and then they will be really stuck, probably having to decompile+extend to add the extra param. What we really want is to take an arbitrary delegate. So long as the compiler can verify that it is well-formed "yes, this is a function", we can handle it. And - crucially - we can give a really nice error message when they do something unsupported, letting them know why it's a problem and what they can do to fix it. |
Beta Was this translation helpful? Give feedback.
-
You can do this today by having the method accept delegate. The consumer then has to define their own delegate type, create a lambda and assign it to a delegate variable of that type, and then pass it to the method. Klunky, yes, but that's what the language supports today. The timeframe for adding any new features to the language is not short. Many of the bigger features slated to be released in C# 8.0 have been in the works for years. And the team is currently heads-down trying to wrap up C# 8.0, so nothing new is going to be considered at the moment. Even if this got championed immediately and slated for the very next C# release you're likely not going to see something until next year. Getting championed will likely require demonstrating use cases outside of DOTS, particularly given what you plan on doing with these delegates is so far removed from what normal developers will be able to do. |
Beta Was this translation helpful? Give feedback.
-
Honestly I think this may be the best approach thus far. By delegating (haha) the declaration of the delegates to the user instead of the API itself, you won't need 6,500 predefined delegates. It wouldn't be hard at all to clearly document either. This also means the user can supply attributes via the delegate, which I'm sure would be useful down the line. |
Beta Was this translation helpful? Give feedback.
-
Similarly you could use a generic type parameter constrained to public delegate void ApplyVelocities(ref Position position, in Velocity velocity);
public class C {
public void M() {
EntitiesForEach
.WithBurst(true)
.WithSupportStructuralChanges(false)
.WithName("ApplyVelocities") // maybe infer from the delegate name?
.WithCode<ApplyVelocities>((ref Position position, in Velocity velocity) =>
{
position.Value += velocity.Value;
});
}
} That would give the consumer the flexibility to define literally any combination of parameters that they would want, generically or not, but at the price of having to define the delegate. Maybe you could offer some overloads of those predefined delegates but to a smaller arity and combination to keep the delegate count reasonable but have this overload as an escape hatch for more complicated scenarios, like more than 8 arguments. |
Beta Was this translation helpful? Give feedback.
-
Variadic generics could be very nice, but not used too often |
Beta Was this translation helpful? Give feedback.
-
Now that lambdas have a natural type in C# 10, this can be done really nicely without the need to generate any delegates: public EntitiesForEach WithCode<T>(T code)
where T : Delegate
{
// ...
return this;
}
// ...
EntitiesForEach
.WithBurst(true)
.WithSupportStructuralChanges(false)
.WithName("ApplyVelocities")
.WithCode((ref Position position, in Velocity velocity) =>
{
position.Value += velocity.Value;
}); The C# compiler will generate a delegate type automatically, and the Burst compiler can then query it from the IL. |
Beta Was this translation helpful? Give feedback.
-
Excuse me if this suggestion is bad, I don't have much experience. correct me if I get anything wrong. As far as I understand you have a lot of overloads of different delegates because you wanna accept different arguments and you wanna reduce the amount of overloads. Couldn't you create a Delegate that accepts any arguments? Then you only have to overload it for the different return types but no overloads for anything param/argument related anymore: delegate void MyVoidMethod(params object[] args);
delegate TReturn1 MyReturn1Method(params object[] args);
delegate TReturn2 MyReturn2Method(params object[] args); or make this thing return a Generic type, potentially one that all the returntypes have in common as their base?
if you don't want all to have Another idea that came to my mind is constructing the lambda at runtime, there are different types that represent lambdas, for example I think the moment that you decide for a Func<> / Action<> / Predicate<> type you are already too limited and narrowed in what you can do with it regarding the method signature of the delegate or lambda for your usecase which is how you kind of forced yourself unnecessarily to create these 6500 thingies. I dont know if any of those ideas is useful but I am confident there is a clever workaround for those 6500 overloads within the language without any new language feature. |
Beta Was this translation helpful? Give feedback.
-
In Unity's new DOTS C# high performance game engine, we support this syntax to allow users to tell the system they want to run an data transformation on all entities that have a Position component and a Velocity component.
Our buildsystem recognizes this pattern, and does some IL postprocessing, by virtue of understanding the users intent.
Notice that the lambda expression that WithCode() takes, has one ref and one in parameter.
This makes the jobsystem that runs these transformations realize it can schedule this transormation in parallel with other readers of Velocity component, but not in parallel with other transformations that need to access Position, since that will be written to, as it's marked ref, not in.
We need WithCode() to be able to take any amount of parameters ideally, but defenitely not less than 8. We also would like to give the user full freedom to mark each parameter as ref, in, or by value. In order to let users write code like this, we today have to create custom delegates like this:
there are 6500 more where those came from. I've put them up here for reference: https://gist.github.com/lucasmeijer/e7e7c6ae10ef78adaae9b505ca615470
for each delegate, we need to make a custom overload of the WithCode() function.
It would be great if we wouldn't have to do that. It makes visualstudio slow, resharper slow, horrible for docs, etc.
One possible solution that comes to mind is having some kind of special delegate type that any lambda expression is allowed to be assigned to. It's fine for our purposes if that instances of that delegate type are not invokable. We just need the syntax to be valid.
We'd happily welcome any other ideas, from language features to anything else.
Beta Was this translation helpful? Give feedback.
All reactions