DEV Community

Universally quantified types in C#

Brian Berns on March 14, 2020

Generic types Generic types in C# are universally quantified. That means, for example, that List<T> can contain elements of type T...
Collapse
 
costinmanda profile image
Costin Manda • Edited

Your problem is interesting. It feels like it should work, but it doesn't.
The question is really, why can't I convert IList<int> to IList<T>? Or IList<object> for that matter, since int is an object. Let's assume we could:

var ints = new List<int> { 1,2,3 };
var objs = (IList<object>)ints;
Enter fullscreen mode Exit fullscreen mode

Only now I should be able to do objs.Add(new object()), and that clearly is not the case.

For your specific problem, you don't actually use the type of the list in any way, so you need to use a base type that is not generic, like System.Collections.IList:

        private static int SumWeights(
            IList<int> ints, 
            IList<string> strs, 
            Func<IList, int> getWeight) 
            => getWeight((IList)ints) + getWeight((IList)strs);
Enter fullscreen mode Exit fullscreen mode

And I know your objection would be that you didn't mean specifically IList<T>, but anything, which might not have a non generic base class. Perhaps the feature in C# that you are looking for is something like this:

        private static int SumWeights(
            IList<int> ints, 
            IList<string> strs, 
            Func<IList<>, int> getWeight) 
            => getWeight(ints) + getWeight(strs);
Enter fullscreen mode Exit fullscreen mode

But that's illegal in C#. A working solution could be using Func<object,Type,int> and then you would call it like getWeight(ints,typeof(IList<>)), which is legal, only now you have to do a lot of reflection in getWeight.

Collapse
 
shimmer profile image
Brian Berns • Edited

Thanks for your response. Can you make it work with a generic getWeight (i.e. one that takes an IList<T>) and without using reflection? It can be done!

Collapse
 
costinmanda profile image
Costin Manda • Edited

Hmm... there are multiple ways to do it. I don't like any of them. This might work:

private static int SumWeights(
            IList<int> ints, 
            IList<string> strs, 
            Func<dynamic,int> getWeight) 
            => getWeight(ints) + getWeight(strs);
...
Console.WriteLine(SumWeights(ints, strs, x=>getWeight1(x)));
Thread Thread
 
shimmer profile image
Brian Berns • Edited

You’re on the right track with your idea to insert something that prevents the generic-ness of getWeight from bubbling up to SumWeights. However, it can be done safely, without going around the compiler’s type checker via dynamic.

Thread Thread
 
costinmanda profile image
Costin Manda

You can't pass a generic function as a parameter without having your own function be generic. Therefore you need to encapsulate it. Like in an interface. This would be the best design for your problem.

    interface IWeighter
    {
        int getWeight<T>(IList<T> list);
    }

But you want to pass a function as a parameter, so probably this ain't it. I have the feeling that whatever you're proposing will not sit well with me.

Thread Thread
 
shimmer profile image
Brian Berns

You got it. That interface is exactly what I'd suggest. You're still passing in the function, just with one level of indirection. It's a bit more verbose, but not too bad, I think. Nice work!

Collapse
 
slavius profile image
Slavius

Why would you try to introduce another problem and pull code from a different domain?

// getWeight() is a method specific to the Interface IList<T>
static int SumWeights(
    IList<int> ints,
    IList<string> strs)
    => ints.getWeight() + strs.getWeight();
Enter fullscreen mode Exit fullscreen mode
Collapse
 
shimmer profile image
Brian Berns

What if there were multiple different versions of getWeight, and you wanted to SumWeights to work with any of them? You'd have to pass it in somehow as an argument, right?

Collapse
 
slavius profile image
Slavius • Edited

Aren't you contradicting yourself? Shouldn't pure function always return the same result with the same input? What exactly you mean by "multiple different getWeight() functions"? Each domain implements only one existing version from the Interface. There can be differences but only in input parameters to the getWeight() method which are handled by the compiler inference.

You said you need to sum the weights. While it is possible to sum int and float by doing implicit conversion, there cannot exist implementation of an interface method with different output type.

While what you propose introduces domain modelling problem. Imagine a new developer tasked to implement IList<float>. When he's finished implementing the interface your code is still broken because he has no idea about your Func<IList<>, int> getWeight() method where he needs to additionally implement new case/switch to handle float sums.

Thread Thread
 
shimmer profile image
Brian Berns

I think you're making this more complex than it needs to be. I'm just trying to create a scenario where you're passing a generic function (getWeight) to a non-generic method (SumWeights).

Thread Thread
 
slavius profile image
Slavius • Edited

Are you talking about IoC / DI? ;)

Edit: Now I remember! Your solution reminds me of Service locator pattern, which is BTW an anti-pattern. That's why I found it immediately wrong.

Collapse
 
shimmer profile image
Brian Berns • Edited

One of the requirements is that you have to pass a generic getWeight to SumWeights somehow. Imagine that there are multiple different implementations of getWeight that all have the same type signature. How do you tell SumWeights which version to use?