Skip to content
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

Champion: "Partial Type Inference" #1349

Open
5 tasks
gafter opened this issue Mar 1, 2018 · 86 comments
Open
5 tasks

Champion: "Partial Type Inference" #1349

gafter opened this issue Mar 1, 2018 · 86 comments
Assignees
Labels
Milestone

Comments

@gafter
Copy link
Member

gafter commented Mar 1, 2018

  • Proposal added
  • Discussed in LDM
  • Decision in LDM
  • Finalized (done, rejected, inactive)
  • Spec'ed

See #1348

Currently, in an invocation of a generic method, you either have to specify all of the type arguments or none of them, in the latter case depending on the compiler to infer them for you. The proposal is to permit the programmer to provide some of the type arguments, and have the compiler infer only those that were not provided.

There are (at least) three possible forms this could take:

  1. Named type arguments (Discussion: Named type arguments #280, Proposal: Allow partial generic specification #1348), e.g. M<TElement: int>(args)
  2. Omitted type arguments separated by commas, e.g. M<int, >(args)
  3. Types to be inferred specified by var (Proposal: Allow partial generic specification #1348), e.g. M<int, var>(args)

Design Meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-07.md#partial-type-inference

@bondsbw
Copy link

bondsbw commented Mar 1, 2018

Pros/cons for 2 vs 3?

Both are DRY but I prefer 2... you don't buy much visually with 3.

@jnm2
Copy link
Contributor

jnm2 commented Mar 1, 2018

1 is okay but verbose. I prefer 2 because I would like this potential progression in levels of needed disambiguation for Foo<TKey, TValue>, assuming Foo<> or Foo also existing or not existing:
Foo<TKey,>, Foo<,>, Foo.

@Logerfo
Copy link
Contributor

Logerfo commented Mar 1, 2018

I'm pretty sure there was an issue regarding recursive generic, but I can't find it. Without recursive type parameters, I think 2 is better because is less verbose, since verbosity is part of the issue here. But with recursive type parameters, I think 1 would fit better.

@TheUnlocked
Copy link

TheUnlocked commented Mar 1, 2018

I think that either 2 or 3 is the best one. 2 is certainly the least verbose (which is nice), but 3 looks more complete and full. I don't think the slight verbosity of 3 would really be an issue because it still requires no thinking of what the type name is, and is only three keystrokes.

I slightly prefer 3, but 2 is also perfectly fine. 1 is a bit too verbose.

@bondsbw
Copy link

bondsbw commented Mar 1, 2018

I support 1 even with the inclusion of 2 or 3. It helps to document the parameters.

@vladd
Copy link

vladd commented Mar 2, 2018

I believe, F# is using _:

> open System.Collections.Generic;;
> let s = [ 1; 2; 3 ] :> IEnumerable<_>;;
val s : IEnumerable<int> = [1; 2; 3]
> let l = List<_>([1; 2; 3]);;
val l : List<int>

(So we could consider using it too.)

@qrli
Copy link

qrli commented Mar 2, 2018

Isn't 2 already used in other constructs?

typeof(IDictionary<,>)

@jveselka
Copy link

jveselka commented Mar 2, 2018

@qrli
typeof(IDictionary<,>) gets you open generic type. However this proposal is about infering fixed type parameters. So I think symmetry is not necessary.

@qrli
Copy link

qrli commented Mar 2, 2018

@zippec typeof(IDictionary<string,>) is also valid open generic.

@jnm2
Copy link
Contributor

jnm2 commented Mar 2, 2018

@qrli Say what? Type objects cannot have partially-open generics.

@federicoce-moravia
Copy link

@jnm2 IIRC typeof expressions cannot have partially-open generics but you can construct such Types via reflection.

@jnm2
Copy link
Contributor

jnm2 commented Mar 2, 2018

@federicoce-moravia I don't believe either is possible. Can you show me?

Btw, closing over string, TValue is still 100% closed, not partially closed. There is an important difference between an open generic and a generic that is closed over TKey, TValue.

@HaloFour
Copy link
Contributor

HaloFour commented Mar 2, 2018

@jnm2

You can do it by grabbing the generic type parameter from the open generic type:

Type type = typeof(Dictionary<,>);
Type[] args = type.GetGenericArguments();
Type type2 = type.MakeGenericType(typeof(string), args[1]);

@jnm2
Copy link
Contributor

jnm2 commented Mar 2, 2018

@HaloFour That's typeof(Dictionary<string, TValue>) which is completely closed over those two types. For example.

@HaloFour
Copy link
Contributor

HaloFour commented Mar 2, 2018

@jnm2

It's something that cannot be expressed in C#. It is typeof(Dictionary<string,>) because the second type parameter is still the open generic type argument from the open generic type Dictionary<,>. The Type.ContainsGenericParameters property returns true (as it does with open generic types) and you cannot instantiate that type.

@jnm2
Copy link
Contributor

jnm2 commented Mar 2, 2018

@HaloFour I understand that it inexpressible in C#. Here's the dillema: it is fully constructed to close over typeof(Foo<TValue, TKey>). Why should it not also be fully constructed closing over typeof(Foo<TValue, TValue>) or typeof(Foo<TKey, TValue>)?

public static class Program
{
    public static void Main()
    {
        new Foo<string, int>().CloseForwards();
        new Foo<string, int>().CloseBackwards();
    }
}

public class Foo<TKey, TValue>
{
    public void CloseForwards()
    {
        // This does the equivalent of:
        _ = typeof(Foo<TKey, TValue>);

        var openGeneric = typeof(Foo<,>);
        var fooTypeArguments = openGeneric.GetGenericArguments();
        var closeOverFooTypeArguments = openGeneric.MakeGenericType(fooTypeArguments[0], fooTypeArguments[1]);

        Console.WriteLine(openGeneric == closeOverFooTypeArguments); // true (!)
        Console.WriteLine(closeOverFooTypeArguments.IsConstructedGenericType); // false (!)
    }
    
    public void CloseBackwards() 
    {
        // This does the equivalent of:
        _ = typeof(Foo<TValue, TKey>);
        
        var openGeneric = typeof(Foo<,>);
        var fooTypeArguments = openGeneric.GetGenericArguments();
        var closeOverFooTypeArguments = openGeneric.MakeGenericType(fooTypeArguments[1], fooTypeArguments[0]);

        Console.WriteLine(openGeneric == closeOverFooTypeArguments); // false
        Console.WriteLine(closeOverFooTypeArguments.IsConstructedGenericType); // true
    }
}

So I'm not talking about being able to instantiate from a separate class. I'm talking a level more abstract: what can you specify using typeof inside the definition of the type which declares TKey and TValue?

@HaloFour
Copy link
Contributor

HaloFour commented Mar 2, 2018

@jnm2

I think we're walking in a weird gray area not well handled by the BCL and more academic than practical.

The documentation for IsConstructedGenericType states the following:

Gets a value that indicates whether this object represents a constructed generic type. You can create instances of a constructed generic type.

Whereas the remarks of ContainsGenericParameters states the following:

Since types can be arbitrarily complex, making this determination is difficult. For convenience and to reduce the chance of error, the ContainsGenericParameters property provides a standard way to distinguish between closed constructed types, which can be instantiated, and open constructed types, which cannot. If the ContainsGenericParameters property returns true, the type cannot be instantiated.

Per the documentation it would seem that it wouldn't be legal for those two properties to both return true. I'd almost call it a bug.

Either way, I don't think this should hold up any conversation about a placeholder syntax (or lack thereof) to denote a generic type argument that is inferred.

@jnm2
Copy link
Contributor

jnm2 commented Mar 2, 2018

Either way, I don't think this should hold up any conversation about a placeholder syntax (or lack thereof) to denote a generic type argument that is inferred.

Certainly not. 😁

@FunctionalFirst
Copy link

@gafter Is omitting the type args altogether another option? F# supports the syntax let d = Dictionary() for generic dictionary.

@gafter
Copy link
Member Author

gafter commented Mar 2, 2018

@FunctionalFirst We infer the type arguments to methods, not to types. This is about method type argument inference.

@alrz
Copy link
Member

alrz commented Mar 2, 2018

@gafter You mean this doesn't cover constructors? I think there are a few other proposals about that and type inference in general (for instance: #92). I hope this can led to considering those as well.

@BhaaLseN
Copy link

BhaaLseN commented Mar 3, 2018

Any thoughts on methods that have more than 2 type parameters and omitting multiple trailing ones? My personal feeling: No, since they're not optional like default-parameters
With M<T1, T2, T3, T4>(...), is any of the following permitted?

  • M(...) (no angle-brackets at all, we can use this today if type parameters can be fully inferred)
  • M<>(...) (no mention of any type parameters, not sure if this has any use whatsoever)
  • M<int>(...) (T1 is int, infer all the rest. Chances are we meant the first parameter, but did we really?)
  • M<int, >(...) (T1 is int, infer all the rest. Looks more like we meant the first parameter, but would probably be ambiguous if there are multiple overloads with a different number of type parameters)
  • M<T1: int>(...) (T1 is int, infer all the rest. Specific, concise, but again the potential overload issue)
  • M<int,,,>(...) (T1 is int, infer all the rest, empty space with comma for each of them. Number and placement of parameters is clear)
  • M<int, var, var, var>(...) (T1 is int, infer all the rest, var for non-specified ones. Number and placement of parameters is clear)

Also:

  • M<T1: int, T4: string>(...) (T1 is int, T4 is string, infer all the rest. Potential overload issue for multiple candidates)
  • M<int,,, string>(...) (T1 is int, T4 is string, infer all the rest, empty space with commas. Probably won't work without the commas for position indication)
  • M<int, var, var, string>(...) (T1 is int, T4 is string, infer all the rest, var for non-specified ones. More verbose than the previous, but nicer than just "empty" commas)

For that, I'm leaning more towards 3 than 2 (while 2 is the current way of specifying an open generic with typeof). Multiple commas next to each other, with no real content don't resonate with me at all, even when the var in there makes it pretty verbose.
I'm personally not a friend of named arguments (mostly because I'm not a friend of default parameters; they've far too often bit me in the behind in shared code bases, but that's not the discussion here), but I can see merit for 1 when the number of generic parameters reaches a certain number (which might as well be some other indication of code-smell or similar, with very few exceptions)

@mpawelski
Copy link

mpawelski commented Mar 6, 2018

Great that language team is considering improving type inference in C#. This is something I've been waiting for a long time.

Basically I think C# should just copy get inspiration from F# type inference. I mean, when it comes to local variable inference in function's body, not full Hindley Milner function's argument type inference.

Type inference in F# was quite intuitive for me. I just discovered it naturally. In C# we recently got _ wildcard so it wasn't used that often hence this might not be such easily discoverable as in F# when you expect things to just get inferred and sometimes put _ to help compiler.

Anyway, I like F# syntax a lot and for me it's good if similar features have similar syntax in both languages.

Partial type inference

F#:

let testGeneric<'T1,'T2>(arg1 : 'T1) =     
    printfn "T1: %s T2: %s" (typeof<'T1>).FullName (typeof<'T2>).FullName
    () //returns nothing

testGeneric<_, string>(1)

result printed:
T1: System.Int32 T2: System.String

The inspired syntax in C# could look like that:

    class Program
    {
        static void testGeneric<T1, T2>(T1 arg1)
        {
            Console.WriteLine("T1: {0} T2: {1}", typeof(T1).FullName, typeof(T2).FullName);
        }

        static void Main(string[] args)
        {            
            //doesn't work but could (now I need to write testGeneric<int, string>(1);)
            testGeneric<_, string>(1); 
        }
    }

Because some people mentioned other possible type inference features I'll try to show how F# type inference could work in C#.

Initialization based on later methods call

F#:

let list = new System.Collections.Generic.List<_>()
list.Add(1)
list.Add(2)

let list2 = System.Collections.Generic.List()
list2.Add(1)
list2.Add(2)
//list2.Add("foo") 
//error if commented out
//This expression was expected to have type 'int' but here has type 'string'
let list2 = System.Collections.Generic.List<_>()
// Error:
// Value restriction. The value 'list2' has been inferred to have generic type
//    val list2 : Collections.Generic.List<'_a>    
// Either define 'list2' as a simple data term, make it a function with explicit arguments or, if you do not intend for it to be generic, add a type annotation.

list is inferred to be Collections.Generic.List<int>.

Notice the errors when you call generic methods with object of different type or don't call any method that helps compiler know what to infer.

Hypothetical C# syntax:

// Should be the same syntax (just use `var` instead of let and don't skip `new` keyword)

Constructors type inference

Type inference for calling constructors should be the same as for calling function.

F# syntax:

let arrayOfNumbers = [| 1;2;3;4|]
let listOfNumbers = new System.Collections.Generic.List<_>(arrayOfNumbers);
//or don't use "new" (more common in F#) and skip passing wildcard ("_") as type parameter
let listOfNumbers2 = System.Collections.Generic.List(arrayOfNumbers);

listofNumbers and listofNumbers2 has type List<int>

Hypothetical C# syntax:

var arrayOfNumbers = new[] { 1, 2, 3, 4 };
var listOfNumbers = new System.Collections.Generic.List(arrayOfNumbers);

It's very annoying to have to specify this type in constructor. For example why I can't write this?:

var listOfAnonymousTypes = new[] { 1, 2, 3, 4 }.Select((number, index) => new { number, index });
var hashSetOfAnonymousTypes = new HashSet(listOfAnonymousTypes);

Inference from Object and Collection Initializers

F# doesn't have the same Object and Collection Initializers syntax as C#.

However, F# has similar syntax to object initializer when initializing immutable record objects and we can also initialize mutable properties of class similar to how we set properties for attributes in C# (link)

F# syntax (record):

type SomeTypeWithImmutableProperties<'T1,'T2> = {
    Foo1 : 'T1;
    Bar2 : 'T2;
}

let immutableObject = { Foo1 = 1; Bar2 = "bar" }

type of immutableObject is SomeTypeWithImmutableProperties<int,string>

F# syntax (class):

type SomeClassWithMutableProperties<'T1,'T2>() = 
    member val Foo3 : 'T1 = Unchecked.defaultof<'T1> with get, set
    member val Bar3 : 'T2 = Unchecked.defaultof<'T2> with get, set

let someMutableObject1 = SomeClassWithMutableProperties(Foo3 = 1, Bar3 = "bar");
let someMutableObject2 = SomeClassWithMutableProperties<_,_>(Foo3 = 1, Bar3 = "bar");

type of someMutableObject1 and someMutableObject2 is of type SomeClassWithMutableProperties<int,string>

Hypothetical C# syntax:

class SomeClassWithMutableProperties<T1,T2>
{
    public T1 Foo { get; set; }
    public T2 Bar { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var someObject1 = new SomeClassWithMutableProperties
        {
            Foo = 1,
            Bar = "bar"
        };

        var someObject2 = new SomeClassWithMutableProperties<_,_>
        {
            Foo = 1,
            Bar = "bar"
        };
    }
}

C# has also nice syntax for collection initializers, why not allow this code to work?:

var list1 = new List { 1, 2, 3 };
var list2 = new List() { 1, 2, 3 };
var list3 = new List<_> { 1, 2, 3 };
var list4 = new List<_>() { 1, 2, 3 };

Or with index initializers it could look like this (and infer variables to be Dictionary<int,string>):

var dict1= new Dictionary()
{
    [1] = "Please",
    [2] = "implement",
    [3] = "this :)"
};
var dict2 = new Dictionary
{
    [1] = "It",
    [2] = "would",
    [3] = "be awesome!"
};
var dict3 = new Dictionary<_, _>()
{
    [1] = "Let's",
    [2] = "make",
    [3] = "C#"
};
var dict4 = new Dictionary<_, _>
{
    [1] = "great",
    [2] = "again!",
    [3] = ";-)"
};

This one should be an error:

var dict5 = new Dictionary
{
    [1] = "",
    ["foo"] = 1,
};

Probably type inference should work similar as type inference for implicitly-typed arrays (new[]{} syntax)

Return Type Inference

Mentioned in #92

F#:

let row<'T1>(name : string) : 'T1 = 
    let objToReturn = Unchecked.defaultof<'T1>
    objToReturn
    
let id: int = row("id")
let name: string = row("name")
let birthDate: DateTime = row("birthDate")

why not allow the same in C#: (example from #92)

int id = row.Field("id"); // legal, generic type argument inferred to be int
var id = row.Field("id"); // not legal, circular inference
row.Field("id"); // not legal, no assignment to a type

All of the example are working in F# not because it's fundamentally different language and it won't fit in C# style. I believe all of this proposed features would fit nicely in C# and greatly improve day-to-day work of C# programmer.

@jnm2
Copy link
Contributor

jnm2 commented Mar 6, 2018

Would this partially mitigate #129? I've been running up against this one a lot.

@ijsgaus
Copy link

ijsgaus commented Mar 6, 2018

I propose next implementation:

  1. When type of generic parameter can be inferred full, skip angle braces
  2. When compiler can take parameters partial, specify only needed, without special syntax to skipped places - no additional commas.
  3. When compiler see conflict - use _ as possible type replace or concrete type specifier.
  4. When compiler infer type, look on generic type constraint and analyze visible types in scope for find concrete type, otherwise require complete definition.
    Sample (For all):
public interface IAdd<T> 
{
    T Zero { get; }
    T Add(T a, T b);
}

public static class SumAddedd
{
    public static T Fold<TAdd, T>(this IEnumerable<T> en) 
        when TAdd : struct, IAdd<T>
      => en.Aggregate(default(TAdd).Zero, (a, p) => default(TAdd).Add(a, p));
}

Simple inference:

public struct IntAdd : IAdd<int>
{
      public int Zero => 0;
      public int Add(int a, int b) => a + b;
}  

public struct LongAdd : IAdd<long>
{
      public long Zero => 0;
      public long Add(long a, long b) => a + b;
}  

var a : IEnumerable<int>;
var b : IEnumerable<long>;
var c  : IEnumerable<T>;

// This can be inferred
var fa = a.Fold(); // Take <IntAdd, int> 
var fb = b.Fold(); // Take <LongAdd, int> 
var fc = c.Fold<IntAdd>(); // Take <IntAdd, int> 
var fc = c.Fold<long>(); // Take <LongAdd, long> 

Complex inference:

public struct IntAdd : IAdd<int>
{
      public int Zero => 0;
      public int Add(int a, int b) => a + b;
}  

public struct IntMul : IAdd<int>
{
      public int Zero => 1;
      public int Add(int a, int b) => a * b;
}  


public struct LongAdd : IAdd<long>
{
      public long Zero => 0;
      public long Add(long a, long b) => a + b;
}  

var a : IEnumerable<int>;
var b : IEnumerable<long>;
var c  : IEnumerable<T>;

// This can be inferred
var fa = a.Fold(); -- error
 var fa = a.Fold<IntAdd>(); // Take <IntAdd, int> 
 var fa = a.Fold<MulAdd>(); // Take <MulAdd, int> 
var fb = b.Fold(); // Take <LongAdd, int> 
var fc = c.Fold<IntAdd>(); // Take <IntAdd, int> 
var fc = c.Fold<long>(); // Take <LongAdd, long> 

@paulirwin
Copy link

paulirwin commented Mar 14, 2018

I'm in favor of 1 and 3, but 3 only if something like F#'s _ is used instead of var. var does not seem to be an appropriate name for this given that (a) nearly everyone will read that as "variable" and (b) type arguments are not necessarily used for variables. I know that's bikeshedding a little bit, but when a suitable alternative like underscore is available and straightforward I'd suggest adopting that before creating a proposal. Underscore has the additional benefits of being less characters than var (better reflecting the benefit of type argument inference) as well as having no English meaning.

@rcmdh
Copy link

rcmdh commented Mar 15, 2018

The only reason I suggested var was (in my head) it's similarity to using var in variable declaration, and the low cognitive change. "'var' means the inferred type"

var myInt = 3;
// potentially
IEnumerable<var> myEnumerable = new [] { myInt };
var list = new List<var>(myEnumerable);
var casted = list.Select<var, long>(x => x);

@michael-hawker
Copy link

... they have no idea on how many Dictionary types you may have.
... Types don't have similar mechanisms and can only rely on matching by name...

I mean if there's multiple conflicting types included, that's what using statements, aliasing namespaces, and fully quantifying types are for. That problem still exists today if there are multiple Dictionary types in scope. That'd still be a problem and maybe you'd have to say MyNamespace.Dictionary() instead. But if you have one Dictionary type in scope, the compiler can easily forward the generic type arguments from the left-hand side of the statement to the right over for me without me having to re-type them.

Let's just make this simple for folks so we're not repeating generic parameters over and over again everywhere, it's a big pain.

@acaly
Copy link
Contributor

acaly commented Jul 7, 2021

That problem still exists today

True. Today the compiler will let you know whenever there is ambiguity. If you add a new using you either keep all the meaning of your existing code, or have compiler errors. Allowing Dictionary for Dictionary<Key, Value> will turn some error to valid code, and some of the news may refer to different types silently when you add a using. You may need to check all news in the same file manually, or have the compiler tell you where are those ambiguious news, every time you add a new using. (The easiest way for compiler to let you know is to produce an error or warning, so basically don't allow that.)

@IanKemp
Copy link

IanKemp commented Jul 8, 2021

... they have no idea on how many Dictionary types you may have.
... Types don't have similar mechanisms and can only rely on matching by name...

I mean if there's multiple conflicting types included, that's what using statements, aliasing namespaces, and fully quantifying types are for. That problem still exists today if there are multiple Dictionary types in scope. That'd still be a problem and maybe you'd have to say MyNamespace.Dictionary() instead. But if you have one Dictionary type in scope, the compiler can easily forward the generic type arguments from the left-hand side of the statement to the right over for me without me having to re-type them.

Let's just make this simple for folks so we're not repeating generic parameters over and over again everywhere, it's a big pain.

Thank zod, someone who can actually see the forest for the trees.

@CyrusNajmabadi
Copy link
Member

@IanKemp I've mentioned this already, but please behave in a respectful and constructive fashion.

@paulirwin
Copy link

But if you have one Dictionary type in scope, the compiler can easily forward the generic type arguments from the left-hand side of the statement to the right over for me without me having to re-type them.

Let's just make this simple for folks so we're not repeating generic parameters over and over again everywhere, it's a big pain.

That is still being accomplished with approach 2 from the original post, just it would be typed as Dictionary<,> instead of Dictionary. No having to repeat generic parameters, just three extra characters (in this case). Avoids the type collision issue, and makes readers of the code know that it is a generic type with arity 2.

Another use case: I spend a fair amount of time porting Java code to C#, and due to Java's type erasure, it's often convenient to have a non-generic base class with the same name as the generic type.

@IanKemp
Copy link

IanKemp commented Jul 8, 2021

@IanKemp I've mentioned this already, but please behave in a respectful and constructive fashion.

How is agreeing with someone disrespectful and/or unconstructive?

Also you should try reading your own Code of Conduct sometime:

A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate.

You haven't complied with either of the bolded sections.

@Happypig375
Copy link
Member

@IanKemp Are you that excited to receive an official warning?

@jnm2
Copy link
Contributor

jnm2 commented Jul 8, 2021

Please let's not allow the thread to continue further off topic while we wait for a moderator to be online.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Jul 8, 2021

You haven't complied with either of the bolded sections.

@IanKemp i cannot send you private messages as I have no other means to contact you.

Regarding 'providing clarity': My initial message specified what the problem is here. We expect people to engage in a friendly and constructive manner here similar to any professional setting.

How is agreeing with someone disrespectful and/or unconstructive?

Consider the difference between statements like:

'i agree with that' vs something like 'finally someone smart who gets it'.

Both are statements of agreement. However one is inherently belittling to others. In this case, your statement seemed purposefully setting out to disparage others for not 'see[ing] the forest for the trees'.

This is all I'll say here on moderation. If you'd like to continue this further, you can reach me at cyrusn@microsoft.com. Thanks!

@IanKemp

This comment has been minimized.

@Happypig375
Copy link
Member

Have you considered implementing the feature yourself and submitting a pull request to Roslyn? Why wait for other people to do the work?

@CyrusNajmabadi
Copy link
Member

@IanKemp Final warning. If you want to take about this further, email me as I mentioned. Discussions about the code of conduct or moderation need to end here.

@CyrusNajmabadi
Copy link
Member

whereby incredibly useful and incredibly simple features like this one languish in proposal hell for the simple reason that a handful of people with obscure and unrealistic use cases are allowed to hold said features hostage. It's been OVER THREE YEARS since this one was suggested and the possibility of seeing it implemented anytime soon appears even more remote.

You have a fundamental misunderstanding here. Nothing has held this feature hostage. We already have champions for it. We just need a proposal here. We don't have one yet as the champions are working on other features and the entire team is overloaded with the current amount of work just to ship c#10.

@CyrusNajmabadi
Copy link
Member

And we need its new features to ship sometime before the next millennium.

New features generally ship yearly (or sometimes multiple times a year). However, the team itself decides what it things is important to front load and what isn't. Currently, while this does have champions, no one particularly feels that this particular issue was important enough for c#10. Will that change for c#11? Depends on how we view this over the thousands of other areas we can make language changes that people have been wanting as well.

@CyrusNajmabadi
Copy link
Member

Have you considered implementing the feature yourself and submitting a pull request to Roslyn? Why wait for other people to do the work?

Just for transparency on this idea: do not do this. An unrequested pr implementing a language change will be summarily rejected.

What you can do though is work to create a viable proposal that, with team permission, could be approved and made into a compiler change. Community members have done this before. However the time commitment and bar is just as high as for anyone directly on the team. So please discuss with team first about taking this path. Thanks!

@Reinms
Copy link

Reinms commented Jul 16, 2021

As long as the first form exists as a fallback I'll be a very happy person. For most cases simple wildcard usage would be best but, much like named method arguments, the merits of the first syntax don't really show through until you are dealing with a very large number of them. Incidentally, those cases when you have many generic parameters are the cases that benefit the most from expanded inference.

Very long real world example

Personally, I work with reflection.emit very often, and I have a set of helper types and extension methods that use generics to do basic verification that the stack is balanced and the right types are going to the right place at compile time. This usually looks something like this:

Return<int> method = new ILHelper(methodBuilder)
    .LdcI4(53)
    .LdcI4(13)
    .Add()
    .Ret();
    
 //Signature of the Add method
 static S<int, TStack> Add<TStack>(this S<int, S<int, TStack>>) where TStack : IStack {}
 

The stack state is stored with a generic type, in this case the type after the 2 ldcI4s but before the add is S<int, S<int, Empty>> which is a real pain to type out, hence the call chaining.

This all works great until you want to emit a call and type inference breaks because you need to supply each part of the function signature individually to the method as generic parameters to keep stack checking, but you also need to distinguish between an Action<T1,T2> and a Func<T1,T2> in order to get an implicit delegate conversion. The end result looks something like this:

Return<int> method = new ILHelper(methodBuilder)
    .LdcI4(1)
    .LdcI4(2)
    .LdcI4(3)
    .LdcI4(4)
    .Call<S<int, S<int, S<int, S<int, Empty>>>>, int, int, int, int, int, SigReturn<int>>(StaticFuncs.Add4)
    .Ret();
//Or
Return method = new ILHelper(methodBuilder)
    .LdcI4(1)
    .LdcI4(2)
    .LdcI4(3)
    .LdcI4(4)
    .LdcI4(5)
    .Call<S<int, S<int, S<int, S<int, S<int, Empty>>>>>, int, int, int, int, int, NoSigReturn>(StaticFuncs.Print5)
    .Ret();

With wildcards alone (option 2 and 3 basically) it would improve, but still require that extra generic param that denotes the return or lack thereof. Looking something like this:

Return<int> method = new ILHelper(methodBuilder)
    .LdcI4(1)
    .LdcI4(2)
    .LdcI4(3)
    .LdcI4(4)
    .Call<var, int, int, int, int, int, SigReturn<int>>(StaticFuncs.Add4)
    .Ret();
//Or
Return method = new ILHelper(methodBuilder)
    .LdcI4(1)
    .LdcI4(2)
    .LdcI4(3)
    .LdcI4(4)
    .LdcI4(5)
    .Call<var, int, int, int, int, int, NoSigReturn>(StaticFuncs.Print5)
    .Ret();

With syntax 1 though, that last arg could be dropped entirely:

Return<int> method = new ILHelper(methodBuilder)
    .LdcI4(1)
    .LdcI4(2)
    .LdcI4(3)
    .LdcI4(4)
    .Call<In1: int, In2: int, In3: int, In4: int, Out: int>(StaticFuncs.Add4)
    .Ret();
//Or
Return method = new ILHelper(methodBuilder)
    .LdcI4(1)
    .LdcI4(2)
    .LdcI4(3)
    .LdcI4(4)
    .LdcI4(5)
    .Call<In1: int, In2: int, In3: int, In4: int, In5: int>(StaticFuncs.Print5)
    .Ret();

Technically, this is more code to write out than just wildcards still, but only by a small amount and the overall clarity of the code is much much better. Additionally, being able to drop that last argument makes the implementation of the Call method way easier to deal with.

Note: Type inference is just one of many headaches when trying to make something like this work. For clarity I've omitted a fair few additional generic arguments that are needed to handle byref and pointer types (Cannot use those normally in generics), and also totally removed all the spooky tuples needed to do branches.

Other tangential note: Implementing something like the above also is quite possibly the fastest possible way to bump your head into every single limitation on generics that exists in C#. If that sounds like fun then I highly recommend it...


I'm also partial to _ as the symbol for a wildcard, for what it's worth. In many IDEs var and primitive types like int get the same highlight color which would be an issue for being able to instantly recognize what is going on.

MyClass<var,int,int,var,int> _ = new();
MyClass<,int,int,,int> _ = new();
MyClass<_,int,int,_,int> _ = new();

As I recall, using _ is also in line with an earlier version of the specification for generics that included a hole type, but I'm a bit fuzzy on that and can't find a link.

@rootflood
Copy link

i think reason of this proposal could be this:

static void Run<t, o, i1, i2, i3>(Func<t, o> f, i1 Input1, i2 Input2, i3 Input3)
{     }
Run <MyApp,,,,> (x => x.ToString(), 1, 2, 3);

@Thaina
Copy link

Thaina commented Oct 27, 2021

I'm not sure this related but, is this issue also involve partial generic for is pattern ?

Is this issue also made this below possible?

object obj = new Dictionary<string,string>();

if(obj is Dictionary<string,> dict) // allow any type of dictionary's value, just need to have string as a key
{
   string key = dict.Keys.FirstOrDefault();
   return dict[key]; // default to object ?
}

@HaloFour
Copy link
Contributor

@Thaina

Is this issue also made this below possible?

No. Partial type inference only means that the compiler fills in part of the generic type arguments automatically, but the types are known at compile time. The runtime doesn't permit use of open generic types in that manner you've described, and it couldn't use variance either in that case.

@TahirAhmadov
Copy link

I would prefer _ as the placeholder - new Dictionary<string, _> { { "abc", new { Prop = "Value", } } } to avoid visual confusion with typeof(Dictionary<,>); but I think just commas would work, too, given that the typeof syntax is not used anywhere else. I think named arguments are too verbose and var keyword doesn't fit linguistically.

@jswolf19
Copy link

jswolf19 commented Jan 7, 2022

VB.net also seems to support partial type inference, at least in a limited fashion. I don't know the specifics and can't seem to find a reference easily, but I know I am able to do the following with extension methods:

Public Module TestExtension
    <Extension>
    Public Function ConvertOutput(Of InT, OutT)(ByVal source As IEnumerable(Of InT)) As IEnumerable(Of OutT)
        Return source.Select(Function(input) DirectCast(Convert.ChangeType(input, GetType(OutT)), OutT))
    End Function
End Module

...

Dim strings As IEnumerable(Of String) = {"1", "2", "3"}
Dim ints As IEnumerable(Of Integer) = strings.ConvertOutput(Of Integer)()

It is possible this is limited to the types that are provided by the this (initial) parameter of extension methods. In fact, providing both types appears to result in an error:

Dim ints As IEnumerable(Of Integer) = strings.ConvertOutput(Of String, Integer)()

@Rekkonnect
Copy link
Contributor

Re-iterating through this proposal, I'm very fond of the idea of inferring obvious type parameters, both for generic methods and type constructors, especially given that generic inference does not apply to constructors.

Gotta note that enabling named type parameters with inference for the others, there will have to be support for handling ambiguities for types with multiple arities. That aside, it seems like a very promising QoL improvement, one step closer to reducing some inconveniences with generics.

@jeme
Copy link

jeme commented Apr 27, 2023

One thing that I ran into which annoys me and I think this could perhaps over, is when a Type parameter constraint actually ends up defining Type parameter so to speak.

Given:

interface ISomeThing<T1, T2> { }

class SomeThingA : ISomeThing<int,string> {}

T Get<T, T1, T2>() where T: ISomeThing<T1, T2> => ...;

When I call Get, given any type. T1 and T2 is already given by the first type due to the constraint. Yet I have to repeat that information anyways, which seems very convoluted, I wished I could just call Get as:

Get<SomeThing1>(); // (T1 = int, T2 = string, we know this from SomeThing1, yet I have to provide that info)
Get<SomeThing1, int, string>(); // But that is sooo redundant, This example might highlight it better:
Get<ISomeThing<int, string>, int, string>();

Seems like this proposal would not save me from specifying the Type Arguments on the Get method, but at least it would save me from specifying them on the call to get which is where the biggest annoyance is.

@FaustVX
Copy link

FaustVX commented Apr 27, 2023

@jeme
What would happen if SomethingA also implement ISomeThing<object, double>, the compiler can't implies what interface you want.
sharplab.io

@jeme
Copy link

jeme commented Apr 27, 2023

@jeme What would happen if SomethingA also implement ISomeThing<object, double>, the compiler can't implies what interface you want. sharplab.io

Normally that would mean only one of them would be an implicit implementation which TECHNICALLY means you could argue you could infer it to that, but that would be a little crazy i guess, so I would rather that it gave the classic:

error CS0411: The type arguments for method 'Get<T, T1, T2>()' cannot be inferred from the usage. Try specifying the type arguments explicitly.

As it's no longer obvious, therefore it cannot be implicitly inferred.


The other example however is not so crazy in my opinion, it's not like the compiler is unaware, after all, it knows exactly which types I can use as it gives an error if I don't specify exactly matching types in this simple case.

Now Covariance / Contravariance will change this a bit as you can then specify a different type parameter than what is implemented.

E.g:

Get<SomeThingX, A, A>(); //Invalid
Get<SomeThingX, A, B>(); //Invalid
Get<SomeThingX, A, C>(); //Invalid

Get<SomeThingX, B, A>();
Get<SomeThingX, B, B>();
Get<SomeThingX, B, C>(); //Invalid

Get<SomeThingX, C, A>();
Get<SomeThingX, C, B>();
Get<SomeThingX, C, C>(); //Invalid

T Get<T, T1, T2>() where T: ISomeThing<T1, T2> => ...;

interface ISomeThing<in T1, out T2> { }

class SomeThingX : ISomeThing<B,B>

class A { }
class B:A { }
class C:B { }

However, I don't really see that as an issue for the inference, if the two second parameters are not provided, it would assume B and B.

@RikkiGibson RikkiGibson self-assigned this May 17, 2023
@TomatorCZ
Copy link

TomatorCZ commented Jun 17, 2023

Hi!
I tried to kick off some idea with details about implementation, examples and half done prototype in the Roslyn fork. See discussion.

@dotnet dotnet locked and limited conversation to collaborators Nov 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests