6

Goal: Generic enumerated type to be the same type when returned.

Note: This works when the types are entered but I don't understand why they can't be inferred.

List<T> then return List<T>

IOrderedEnumerable<T> then return IOrderedEnumerable<T>

ETC

Current method (works only if all types are entered)

public static TEnumerable WithEach<TEnumerable, T>(this TEnumerable items, Action<T> action)
where TEnumerable : IEnumerable<T>
{
    foreach (var item in items) action.Invoke(item);
    return items;
}

Example Only

var list = new List<int>(); //TODO: Mock random values
list.WithEach(x => Console.WriteLine(x)) //Here WithEach ideally returns List<int> following orignal type List<int>
    .OrderBy(x => x) 
    .WithEach(x => Console.WriteLine(x)); //Here WithEach ideally returns IOrderedEnumerable<int> following OrderBy

Making it work

var list = new List<int>(); //TODO: Mock random values
list.WithEach<List<int>, int>(x => Console.WriteLine(x))
    .OrderBy(x => x) 
    .WithEach<IOrderedEnumerable<int>, int>(x => Console.WriteLine(x));

What I'm missing is why C# can't infer the types although the where filter does make the types accurate. I understand why you either supply all or none generic types to methods so please do not point me to those answers.

Edit: If I can't infer the types; then how can I make this more elegant?

Michael Puckett II
  • 6,586
  • 5
  • 26
  • 46
  • 1
    I'll look into the details, but just as a warning: this ends up iterating over the sequence and then returning the same sequence, which *may* not support being iterated over multiple times. When designing LINQ-like methods I try to avoid multiple iteration. It may not be an issue in your case, but I thought I'd mention it before looking into the type inference. – Jon Skeet Dec 04 '18 at 19:51
  • You're right @JonSkeet; ideally iterations would be immutable. I'm just scratching my head into making the method FLUENT and keeping the question simple. – Michael Puckett II Dec 04 '18 at 19:52
  • Why not `yield`? – Kenneth K. Dec 04 '18 at 19:55
  • @KennethK. How would that help anything? – Servy Dec 04 '18 at 19:57
  • @Servy: I've reopened this as I don't think it's quite the same situation, although it's closely related. The "solution" (or at least the best I can come up with) is appropriate for this question but not that one, too. – Jon Skeet Dec 04 '18 at 19:58
  • @Servy Does that not accommodate for what Jon mentioned? – Kenneth K. Dec 04 '18 at 20:01
  • @KennethK.: It wouldn't satisfy the aim of returning the same type as the input. – Jon Skeet Dec 04 '18 at 20:02
  • @JonSkeet Nor did I think it would. I suppose I was more curious about the OP's approach and if there was some reason to not use `yield` here. – Kenneth K. Dec 04 '18 at 20:03
  • 1
    @KennethK.: Given that the stated goal is "Generic enumerated type to be the same type when returned." that sounds like an answer to your question. (As an example, after calling `OrderBy` then `WithEach`, with the OP's design they'd be able to call `ThenBy` - they can't if `WithEach` returns just `IEnumerable`.) – Jon Skeet Dec 04 '18 at 20:06
  • @JonSkeet The question asked why the types aren't inferred (which is exactly what the other question asked and provides in an answer). It didn't ask for a different implementation that allowed type inference, or didn't require type inference, etc. – Servy Dec 04 '18 at 20:20
  • @Servy I'll give you that; I asked specifically but logically Jon knew what I needed. I'll update the question to be a little more flexible because between the answer and the comments below and the article it's all clearing things up. For now; the 'difference implementation' for me is a proper answer and there is enough conversation tied to it to explain why. – Michael Puckett II Dec 04 '18 at 20:23
  • 1
    @Servy: I'd say there's a difference in terms of why the types aren't inferred too though - that question doesn't have the second type parameter *anywhere* in the parameter list; this does, but the argument doesn't help infer it. For type inference, I think it's useful to have multiple questions that are somewhat similar but with subtle differences. – Jon Skeet Dec 04 '18 at 20:27

2 Answers2

9

Type inference in C# is very complicated - just for once, I'm not going to get the spec out to try to step through it, because I'm aware of just how horrible it can become.

I believe the problem is that neither of the parameter/argument combinations gives the compiler enough information to infer T:

  • The TEnumerable items parameter doesn't mention T, so it isn't used to infer T, despite the type constraint
  • The Action<T> parameter would be fine, but the compiler can't make an inference based on the lambda expression you're providing

I can't think of a good change to the method signature that would make exactly your first code work - but you can change how you invoke the method just a little to make it work, by specifying the parameter type in the lambda expression:

var list = new List<int>();
list.WithEach((int x) => Console.WriteLine(x++))
    .OrderBy(x => x) 
    .WithEach((int x) => Console.WriteLine(x));

The downside of that is that it won't work with anonymous types, of course.

One workaround for that downside is a pretty horrible one, but it lets you express the type of T via a parameter instead, when you need to. You change the method signature to:

public static TEnumerable WithEach<TEnumerable, T>(
    this TEnumerable items,
    Action<T> action,
    T ignored = default(T))

If you wanted to call the method with a list of some anonymous type, you could write:

list.WithEach(x => Console.WriteLine(x.Name), new { Name = "", Value = 10 });

... where the final argument would match the anonymous type. That will allow the type of T to be inferred by the final parameter instead of the second one. You can use that for other types of course, but I'd probably stick to using it for anonymous types instead.

That's all a pretty horrible hack, and I don't think I'd actually use it, but if you really, really need this to work with anonymous types, it would cope.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 8
    Your belief is, of course, correct. Basically **all inferences must be made from arguments to formals**, and *then* the constraints are checked. We never make an inference *from a constraint*. We make inferences *from arguments*, and then *verify* that they are consistent with the constraints. – Eric Lippert Dec 04 '18 at 20:05
  • 2
    @EricLippert That makes sense then. Appreciated! – Michael Puckett II Dec 04 '18 at 20:10
  • 2
    That rule -- arguments to formals -- is relaxed slightly in the case of inferring a type parameter that is in the formal parameter list of a delegate type. In that case, as you correctly note, we infer from *formal parameter lists of typed lambdas* to *formal parameter lists of delegates with unfixed type parameters*. – Eric Lippert Dec 04 '18 at 20:10
  • @JonSkeet Thanks, although it does require typing the Type it is prettier than my working approach. – Michael Puckett II Dec 04 '18 at 20:10
  • 2
    @MichaelPuckettII: You're welcome. This is for whatever reason a controversial decision; lots of people think that constraints should be used to help the compiler *make a decision*, rather than what I believe, which is that constraints should be used to *let the developer know when inference is sufficiently unclear that we cannot determine the right answer from the arguments*. For a long, unproductive debate on this issue, see the comments to https://blogs.msdn.microsoft.com/ericlippert/2009/12/10/constraints-are-not-part-of-the-signature/ – Eric Lippert Dec 04 '18 at 20:12
  • @EricLippert I have no strong opinions either way; just knowing the order or logic clears it up for me. I'll read the post and thanks again. – Michael Puckett II Dec 04 '18 at 20:13
  • 2
    @MichaelPuckettII: FYI, Jon's "horrible hack" is a technique sometimes called "cast by example" because the original use case of this technique was used to make a *conversion* that took an *example* of the type you wanted to cast to. https://blogs.msdn.microsoft.com/alexj/2007/11/22/t-castbyexampletobject-o-t-example/ – Eric Lippert Dec 04 '18 at 20:14
  • 1
    @EricLippert: And of course the decision was changed a bit in C# 7.3, in that although the constraints aren't used to infer the type parameters, they're now used as part of choosing the right member in overload resolution instead of only being checked after the member has been chosen. I can't remember whether I sent that late addition to chapter 14 to you ;) – Jon Skeet Dec 04 '18 at 20:17
  • @EricLippert and @JonSkeet. I believe he added that after I chose the answer or I didn't quite catch that part. The decision to type the ```Type``` in the arguments is what I'm excepting for now; it's not perfect but more elegant than my working example for sure. ie. ```WithEach((int x)``` – Michael Puckett II Dec 04 '18 at 20:17
-1

Declare your extension using just T, like so:

public static IEnumerable<T> WithEach<T>(this IEnumerable<T> items,Action<T> action)
{
    foreach (var item in items) action.Invoke(item);
    return items;
}

This has the downside of losing the specific sub-class of IEnumerable that you implement.

It's easy to implement overloads for the specific subclasses you care about:

public static IOrderedEnumerable<T> WithEach<T>(this IOrderedEnumerable<T> items, Action<T> action)
{
    ((IEnumerable<T>)items).WithEach(action);
    return items;
} 

Returning the IEnumerable after iterating it is a bit scary, though. IEnumerables might not be restartable.

gnud
  • 77,584
  • 5
  • 64
  • 78
  • 2
    The point (as I understand it) is that the OP wants to be able to return the same type as the first parameter, so that they can continue to use methods specific to that type (not to `IEnumerable`) later. – Jon Skeet Dec 04 '18 at 19:54
  • Although this will return the same type reference it will not be the same type moving forward. This will return ```IEnumberable``` when I want it to be ```IOrderedEnumerable``` for example. – Michael Puckett II Dec 04 '18 at 19:54