Search code examples
c#.netcovariancecontravariance

Why is Action<Action<T>> covariant?


This is something I'm having a hard time wrapping my head around. I understand that Action<T> is contravariant and is probably declared as such.

internal delegate void Action<in T>(T t);

However, I don't understand why an Action<Action<T>> is covariant. T is still not in an output position. I'd really appreciate it if someone could try to explain the reasoning / logic behind this.

I dug around a little bit and found this blog post which tries to explain it. In particular, I didn't quite follow what was meant here under the "Explanation for covariance of input" subsection.

It is the same natural if the “Derived -> Base” pair is replaced by “Action -> Action” pair.


Solution

  • OK, so first of all let's be clear what you mean by saying that Action<Action<T>> is covariant. You mean that the following statement holds:

    • If an object of reference type X may be assigned to a variable of reference type Y, then an object of type Action<Action<X>> may be assigned to a variable of reference type Action<Action<Y>>.

    Well, let's see if that works. Suppose we have classes Fish and Animal with the obvious inheritance.

    static void DoSomething(Fish fish)
    {
        fish.Swim();
    }
    
    static void Meta(Action<Fish> action)
    {
        action(new Fish());
    }
    
    ...
    
    Action<Action<Fish>> aaf = Meta;
    Action<Fish> af = DoSomething;
    aaf(af);
    

    What does that do? We pass a delegate to DoSomething to Meta. That creates a new fish, and then DoSomething makes the fish swim. No problem.

    So far so good. Now the question is, why should this be legal?

    Action<Action<Animal>> aaa = aaf;
    

    Well, let's see what happens if we allow it:

    aaa(af);
    

    What happens? Same thing as before, obviously.

    Can we make something go wrong here? What if we pass something other than af to aaa, remembering that doing so will pass it along to Meta.

    Well, what can we pass to aaa? Any Action<Animal>:

    aaa( (Animal animal) => { animal.Feed(); } );
    

    And what happens? We pass the delegate to Meta, which invokes the delegate with a new fish, and we feed the fish. No problem.

    T is still not in an output position. I'd really appreciate it if someone could try to explain the reasoning / logic behind this.

    The "input/output" position thing is a mnemonic; covariant type tend to have the T in the output position and contravariant type tend to have the T in the input position, but that is not universally true. For the majority of cases, that's true, which is why we chose in and out as the keywords. But what really matters is that the types can only be used in a typesafe manner.

    Here's another way to think about it. Covariance preserves the direction of an arrow. You draw an arrow string --> object, you can draw the "same" arrow IEnumerable<string> --> IEnumerable<object>. Contravariance reverses the direction of an arrow. Here the arrow is X --> Y means that a reference to X may be stored in a variable of type Y:

    Fish                         -->     Animal  
    Action<Fish>                 <--     Action<Animal> 
    Action<Action<Fish>>         -->     Action<Action<Animal>>
    Action<Action<Action<Fish>>> <--     Action<Action<Action<Animal>>>
    ...
    

    See how that works? Wrapping Action around both sides reverses the direction of the arrow; that's what "contravariant" means: as the types vary, the arrows go in the contra -- opposing -- direction. Obviously reversing the direction of an arrow twice is the same thing as preserving the direction of an arrow.

    FURTHER READING:

    My blog articles that I wrote while designing the feature. Start from the bottom:

    http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/default.aspx

    A recent question about how variance is determined to be typesafe by the compiler:

    Variance rules in C#