Why Must I Provide Explicitly Generic Parameter Types While the Compiler Should Infer the Type

Why must I provide explicitly generic parameter types While the compiler should infer the type?

The specification limits type parameter inference for generic methods to all or nothing. You can't have partial inference.

The rationale is probably simplifying type inference rules (that are already pretty complex, as they have to take into account overloading rules too).

Why can't the compiler infer a generic parameter when there is a generic return value?

It's not a matter of compiler - it's that C# requires that you either explicitly specify all the type arguments or let it infer all of them.

There is no middle ground using the syntax you've attempted, and I imagine it's because if you had a generic method like this:

public void DoSomething<T1, T2>(T1 data, T2 data)
{
// ...
}

And you used it like this:

var obj1 = "Hello!";
var obj2 = "Hello?";
DoSomething<IEnumerable<char>>(obj1, obj2);

The last line could be shorthand for two equally valid things:

DoSomething<string, IEnumerable<char>>(obj1, obj2);
DoSomething<IEnumerable<char>, string>(obj1, obj2);

A different syntax (like <string, ?>) or additional inference rules would have to be put in place to make cases like this meaningful and non-ambiguous. I imagine the design team thought that it wasn't worth it.


Note that if you really want partial generic type inference, there is a common pattern of splitting the call into two calls, with a helper object to hold the information between the calls. This is essentially currying, applied to type parameters.

I'll present the pattern in a form that uses a public interface and an private implementation but if you're not concerned about that, you can skip the interface altogether.

public TResult DoSomething<TResult, TSource>(TSource data)
{
// ...
}

Would become:

public IWrapper<TSource> DoSomething<TSource>(TSource data)
{
return new WrapperImplementation<TSource>(data);
}

Where:

public interface IWrapper<T>
{
TResult Apply<TResult>();
}

class WrapperImplementation<T> : IWrapper<T>
{
private readonly T _source;

public WrapperImplementation(T source)
{
_source = source;
}

public TResult Apply<TResult>()
{
// ...
}
}

The usage is:

DoSomething("Hello").Apply<int>();

Nested Generics: Why can't the compiler infer the type arguments in this case?

Because the algorithm as described in the C# specification doesn’t succeed in this case. Let’s look at the specification in order to see why this is.

The algorithm description is long and complicated, so I’ll heavily abbreviate this.

The relevant types mentioned in the algorithm have the following values for you:

  • Eᵢ = the anonymous lambda (Foo x) => (Bar y) => new Baz()
  • Tᵢ = the parameter type (Func<T1, Func<T2, T3>>)
  • Xᵢ = the three generic type parameters (T1, T2, T3)

Firstly, there’s the first phase, which in your case does only one thing:

7.5.2.1 The first phase


For each of the method arguments Eᵢ (in your case, there’s only one, the lambda):

  • If Eᵢ is an anonymous function [it is], an explicit parameter type inference (§7.5.2.7) is made from Eᵢ to Tᵢ
  • Otherwise, [not relevant]
  • Otherwise, [not relevant]
  • Otherwise, no inference is made for this argument.

I’ll skip the details of the explicit parameter type inference here; it suffices to say that for the call G((Foo x) => (Bar y) => new Baz()), it infers that T1 = Foo.

Then comes the second phase, which is effectively a loop that tries to narrow down the type of each generic type parameter until it either finds all of them or gives up. The one important bullet point is the last one:

7.5.2.2 The second phase


The second phase proceeds as follows:

  • [...]
  • Otherwise, for all arguments Eᵢ with corresponding parameter type Tᵢ where the output types (§7.5.2.4) contain unfixed type variables Xj but the input types (§7.5.2.3) do not, an output type inference (§7.5.2.6) is made from Eᵢ to Tᵢ. Then the second phase is repeated.

[Translated and applied to your case, this means:

  • Otherwise, if the return type of the delegate (i.e. Func<T2,T3>) contains an as yet undetermined type variable (it does) but its parameter types (i.e. T1) do not (they do not, we already know that T1 = Foo), an output type inference (§7.5.2.6) is made.]

The output type inference now proceeds as follows; again, only one bullet point is relevant, this time it’s the first one:

7.5.2.6 Output type inferences


An output type inference is made from an expression E to a type T in the following way:

  • If E is an anonymous function [it is] with inferred return type U (§7.5.2.12) and T is a delegate type or expression tree type with return type Tb, then a lower-bound inference (§7.5.2.9) is made from U to Tb.
  • Otherwise, [rest snipped]

The “inferred return type” U is the anonymous lambda (Bar y) => new Baz() and Tb is Func<T2,T3>. Cue lower-bound inference.

I don’t think I need to quote the entire lower-bound inference algorithm now (it’s long); it is enough to say that it doesn’t mention anonymous functions. It takes care of inheritance relationships, interface implementations, array covariance, interface and delegate co-/contravariance, ... but not lambdas. Therefore, its last bullet point applies:

  • Otherwise, no inferences are made.

Then we come back to the second phase, which gives up because no inferences have been made for T2 and T3.

Moral of the story: the type inference algorithm is not recursive with lambdas. It can only infer types from the parameter and return types of the outer lambda, not lambdas nested inside of it. Only lower-bound inference is recursive (so that it can take nested generic constructions like List<Tuple<List<T1>, T2>> apart) but neither output type inferences (§7.5.2.6) nor explicit parameter type inferences (§7.5.2.7) are recursive and are never applied to inner lambdas.

Addendum

When you add a call to that identify function I:

  • G((Foo x) => I((Bar y) => new Baz()));

then type inference is first applied to the call to I, which results in I’s return type being inferred as Func<Bar, Baz>. Then the “inferred return type” U of the outer lambda is the delegate type Func<Bar, Baz> and Tb is Func<T2, T3>. Thus lower-bound inference will succeed because it will be faced with two explicit delegate types (Func<Bar, Baz> and Func<T2, T3>) but no anonymous functions/lambdas. This is why the identify function makes it succeed.

Java inferred generic types

So after some further research, I found the answer in the textbook Effective Java, by Joshua Block; Item 27: Favor generic methods. What I was looking for was to simplify/reduce the reiteration of type parameters when invoking the generic constructor - i.e. make it less cumbersome and not repeating parameters that are already given.

It is indeed impossible to infer types for a constructor, however there is a way to exploit generic methods to reduce the reiteration and type parameters for constructors - by making generic factory methods for each constructor and infer the type parameters in this way instead.

This is the information that explains this entire situation, the following is quoted out of the textbook:

One noteworthy feature of generic methods is that you needn’t specify the value of the type parameter explicitly as you must when invoking generic constructors. The compiler figures out the value of the type parameters by examining the types of the method arguments. In the case of the program above, the compiler sees that both arguments to union are of type Set , so it knows that the type parameter E must be String. This process is called type inference.

As discussed in Item 1, you can exploit the type inference provided by generic method invocation to ease the process of creating parameterized type instances. To refresh your memory, the need to pass the values of type parameters explicitly when invoking generic constructors can be annoying. The type parameters appear redundantly on the left- and right-hand sides of variable declarations:

// Parameterized type instance creation with constructor`
Map<String, List<String>> anagrams = new HashMap<String, List<String>>();

To eliminate this redundancy, write a generic static factory method corresponding to each constructor that you want to use. For example, here is a generic static factory method corresponding to the parameterless HashMap constructor:

// Generic static factory method
public static <K,V> HashMap<K,V> newHashMap() {
return new HashMap<K,V>();
}

With this generic static factory method, you can replace the repetitious declaration above with this concise one:

// Parameterized type instance creation with static factory
Map<String, List<String>> anagrams = newHashMap();

It would be nice if the language did the same kind of type inference when
invoking constructors on generic types as it does when invoking generic methods. Someday it might, but as of release 1.6, it does not.

Why doesn't C# infer my generic types?

This was originally posted in the question and moved here on the OP's behalf.

For me the best solution was to change the IQueryProcessor interface and use dynamic typing in the implementation:

public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}

// Implementation
sealed class QueryProcessor : IQueryProcessor {
private readonly Container container;

public QueryProcessor(Container container) {
this.container = container;
}

public TResult Process<TResult>(IQuery<TResult> query) {
var handlerType =
typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
dynamic handler = container.GetInstance(handlerType);
return handler.Handle((dynamic)query);
}
}

The IQueryProcessor interface now takes in a IQuery<TResult> parameter. This way it can return a TResult and this will solve the problems from the consumer's perspective. We need to use reflection in the implementation to get the actual implementation, since the concrete query types are needed (in my case). But here comes dynamic typing to the rescue which will do the reflection for us. You can read more about this in this article.



Related Topics



Leave a reply



Submit