Improve generic type parameters inference. #997
Replies: 14 comments 8 replies
-
Related/duplicates: #110, #478, #741, dotnet/roslyn#5023 and dotnet/roslyn#15166. There's even PRs where this work was already done: dotnet/roslyn#6644 and dotnet/roslyn#7850. |
Beta Was this translation helpful? Give feedback.
-
This unfortunate observation describes the problem with doing this: Suppose you have these methods: public static Foo<string, char> AsFoo(this object bar) =>
new Foo<string, char>(bar);
public static Foo<TBar, T> AsFoo(this TBar bar) where TBar : IEnumerable<T> =>
new Foo<TBar, T>(bar); this call would change:
|
Beta Was this translation helpful? Give feedback.
-
And if it didn't I'm not sure you could implement ValueLinq without creating all new As public static int Count<TSource>(this IEnumerable<TSource> source) Would take precedence over public static int Count<TEnumerable, TEnumerator, TSource>(this TEnumerable source)
where TEnumerable : struct, IValueEnumerable<TSource, TEnumerator>
where TEnumerator : struct, IValueEnumerator<TSource> for var c = collection.Count(); (Ideally I'd like the (Though could sorta work by switching |
Beta Was this translation helpful? Give feedback.
-
@agocke is investigating extending type inference to take constraints into account. |
Beta Was this translation helpful? Give feedback.
-
From: #982 (comment) The main issue is lack of inference of generic arguments based on constraints so you need to do: list.Any<List<int>, List<int>.ValueEnumerator>();
list.Count<List<int>, List<int>.ValueEnumerator>(); Rather than list.Any();
list.Count(); To resolve public static int Count<TSource, TValueEnumerator>(this TSource source)
where TSource : IValueEnumerable<TValueEnumerator>
where TValueEnumerator : struct, IValueEnumerator However, running a perf test for a "ValueLinq" https://gist.github.com/benaadams/294cbd41ec1179638cb4b5495a15accf
Its twice as fast and cuts allocations to zero |
Beta Was this translation helpful? Give feedback.
-
My summary here: the break is real, but much of the annoyance is around inference failure -- when there are no other suitable overloads, but the compiler still fails to properly infer your generic arguments. I think the only way to deal with that is re-do overload resolution after an inference failure, this time taking constraints into account. This could be expensive, so we would have to decide whether that's something we want to do, or whether we can accept some overload resolution breaking changes. |
Beta Was this translation helpful? Give feedback.
-
@agocke what about having a compiler option, if not activated everything is normal. but when activated, it should disallow overloads like this void M(object) {}
void M<T, U>(T t) where T : IEnumerable<U> {} and improve generic type inference to the maximum. I mean, this kind of overload is really trash. I don't think one would ever make overload like this or at least won't do it on a regular basis. but when they do, they shouldn't be able to compile with the new option. and new projects by default should have this option enabled by default, because why not :) |
Beta Was this translation helpful? Give feedback.
-
Code depending on compiler options creates a dialect, and the Language Design Team has been hard at work for the past 15+ years to keep dialects as far away from the language as possible. |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
@agocke I know it’s been a while on this issue. But does expensive in this case mean in terms of compilation time or expense of dev time to implement? |
Beta Was this translation helpful? Give feedback.
-
I have a workaround that I have been using for this problem, but it still gets a bit messy so having this inference working in the compiler would be preferable. The workaround is to place the generic type inside a wrapper struct and ensure all the types that are not being inferred are present in the generic types of that struct. For example assume here is a minimal repro of the issue from the OP: var bar = new Bar<int>().AsFoo(); // error CS0411
public interface IBar<T> {}
public record Bar<T>() : IBar<T>;
public record Foo<TBar, T>(TBar Bar) where TBar : IBar<T>;
public static class IBarExtensions
{
public static Foo<TBar, T> AsFoo<TBar, T>(this TBar bar) where TBar : IBar<T>
=> new Foo<TBar, T>(bar);
} To fix it, you create a BarWrapper type and then define the extension method off the wrapper type. var bar = new Bar<int>().Wrap().AsFoo(); // works without error
public interface IBar<T> {}
public record Bar<T>() : IBar<T>
{
public BarWrapper<Bar<T>, T> Wrap() => new(this);
}
public record BarWrapper<TBar, T>(TBar Bar) where TBar : IBar<T>;
public record Foo<TBar, T>(TBar Bar) where TBar : IBar<T>;
public static class IBarExtensions
{
public static Foo<TBar, T> AsFoo<TBar, T>(this BarWrapper<TBar, T> wrapper) where TBar : IBar<T>
=> new Foo<TBar, T>(wrapper.Bar);
} Obviously there is a bit of pain in that you need to constantly wrap the objects to use it, but if you were making a library such as StructLinq, then what you can do is always deal with your objects inside this wrapper. So if you were to define a An example of using this approach for a StructLinq style library is as follows: using System;
static StreamWrapper<ArrayStream<T>, T> CreateStream<T>(T[] array) => new(new(array));
var arrayStream = CreateStream(new int[] { 1, 2, 3 });
var addOneAndDouble = arrayStream.Select(x => x + 1).Select(x => x * 2); // no errors!
public interface IStream<T> {}
public record struct ArrayStream<T>(T[] Array) : IStream<T>;
public record struct SelectStream<TStream, T, TResult>(TStream BaseStream, Func<T, TResult> Selector) : IStream<TResult>
where TStream : IStream<T>;
public record struct StreamWrapper<TStream, T>(TStream Stream) where TStream : IStream<T>;
public static class IStreamExtensions
{
public static StreamWrapper<SelectStream<TStream, T, TResult>, TResult> Select<TStream, T, TResult>(
this StreamWrapper<TStream, T> streamWrapper,
Func<T, TResult> selector)
where TStream : IStream<T>
=> new(new(streamWrapper.Stream, selector));
} |
Beta Was this translation helpful? Give feedback.
-
I know this issue is duplicated all over the place and I'm not sure where to continue the discussion, please point me to the relevant issue if there is a better one. If I have understood the main problem correctly, LDM commented on breaking changes as described in dotnet/roslyn#7850 (comment) I just wonder if we could add something like a new
to:
Or is adding new keywords completely out of the question? What about characters, like a bang or similar added to the constraint?
This issue keeps coming up when we're making our API:s, forcing us to make ugly compromises between usability and type safety. |
Beta Was this translation helpful? Give feedback.
-
Seconding TLabWest's point, explicitly opting-in to implicit type inference would avoid introducing a breaking change. In summary (I was about to start a separate discussion before I saw this thread)It'd be nice to infer generic arguments from constraints:
That currently fails with:
dotnet/roslyn#7850 implements inference. It is rejected as a breaking change:
As a workaround, we could gate this functionality behind an opt-in:
|
Beta Was this translation helpful? Give feedback.
-
Also, as an alternative to IValueEnumerator/IValueEnumerable approach, I've been implementing my StructLinq over enumerators, not enumerables (though I've made my enumerators enumerable, returning themselves):
Which lowers to:
And has the type One should also look at https://github.com/reegeek/StructLinq (MIT license) which has sidestepped this issue with code like:
Presumably they're writing ToStructEnumerable() extension methods per BCL collection, e.g.;
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
When a generic type parameter is constrained with other generic parameters, it is possible to infer both types from constrained generic type. (currently not supported by compiler)
when calling this.
but It should be inferred, because information about
T
already exist in constraint for TBar herewhere TBar : IBar<T>
Beta Was this translation helpful? Give feedback.
All reactions