Understanding C# Covariance And Contravariance (5) Higher-order functions

Understanding C# Covariance And Conreavariance:

In this part generic delegate variance in C# 4.0 will be examined, where some phenomena could be suprising.

Higher-order functions

According to Wikipedia, higher-order functions are functions which do at least one of the following:

  • take one or more functions as an input;
  • output a function.

The first sample is about taking a function as input:

internal delegate void Action();

internal class Program
{
    // Normal function.
    private static void Action()
    {
    }

    // Higher-order function.
    // Its parameter is another function.
    private static void ActionIn(Action action)
    {
        // The parameter is a function, so we can invoke it.
        action();
    }

    private static void Main()
    {
        // Invoke higher-order function,
        // passing another function as a parameter.
        ActionIn(Action);
    }
}

 

Here ActionIn() is a higher-order function, because its input (parameter) is a function.

The second sample is about returning a function:

internal delegate object Func();

internal class Program
{
    // Func is a normal function.
    private static object Func()
    {
        return new object();
    }

    // FuncOut is a higher-order function.
    // Its return value is another function.
    private static Func FuncOut()
    {
        return () => new object();
    }

    private static void Main()
    {
        // Invoke higher-order function, it returns a function to us.
        // Then the return value can be invoked.
        Func func = FuncOut();
        func();
    }
}

Covariance of input

In the previous parts this is emphasized again and again:

  • Covariance is about output: just like for [T Func<out T>()], T is only used for output, so T is covariant;
  • Contravariance is about input: just like for [void Action<in T>(T obj)], T is only used for input, so T is contravariant.

Now let’s try some variance:

// T is only about input,
// and has nothing to do with output.
// The in keyword indicates T is contravariant,
// or contravariance can happen to Action<in T>.
internal delegate void Action<in T>(T t);

internal class Program
{
    private static void Main()
    {
        Action<Derived> derivedAction = arg => { };
        Action<Base> baseAction = arg => { };

        // Contravariance of input: Action<Base> "is a" Action<Derived>.
        // Or T is contravariant for Action<in T>.
        derivedAction = baseAction;
    }
}

This is quite obvious.

Now something more fancy can be done: making up a “higher-order delegate”, ActionIn<T>(Action<T>). It takes a Action<T> function as a parameter, so it is of higher order.

// T is only about input, and has nothing to do with output.
// T is expected to be still contravariant,
// contravariance can happen to ActionIn<in T>.
// So the in keyword is expected to work as usual.
internal delegate void ActionIn<in T>(Action<T> action);

internal class Program
{
    private static void Main()
    {
        // In the above code, Action<in T> is replaced by ActionIn<in T>.
        ActionIn<Derived> derivedActionIn = arg => { };
        ActionIn<Base> baseActionIn = arg => { };

        // Here the usual contravariance of input is expectd.
        derivedActionIn = baseActionIn;
    }
}

Only some replacement happens. It looks everything is Ok, but the above code cannot be compiled.

In fact, to make it work, the in keyword is needed to be changed into out:

// T is input. T is not output.
// T is contravariant.
internal delegate void Action<in T>(T t);

// T is input. T is not output.
// T is covariant. "in" cannot compile.
internal delegate void ActionIn<out T>(Action<T> action);

internal class Program
{
    private static void Main()
    {
// These are normal functions. Action<Derived> derivedIn = arg => { }; Action<Base> baseIn = arg => { }; // Contravariance of input: Action<Base> "is a" Action<Derived>. derivedIn = baseIn; // These are higher-order functions.
ActionIn<Derived> derivedActionIn = arg => { }; ActionIn<Base> baseActionIn = arg => { }; // Covariance of input: ActionIn<Derived> "is a" ActionIn<Base>. // "derivedActionIn = baseActionIn" cannot compile. baseActionIn = derivedActionIn; } }

Now it works.

Is this a surprise? For [void ActionIn<out T>(Action<T>)], T has nothing to do with output (output is definitely void), why we can apply a “out” keyword to T? Why T is considered as covariant?

Explanation for covariance of input

Basically, an ActionIn<T>(Action<T>) can be considered as a Action<Action<T>>. Because they are both higher-order functions, taking a Action<T> as a input, and returning void.

Once again, check this normal sample:

// Derived "is a" Base.
Action<Derived> derivedAction = arg => { };
Action<Base> baseAction = arg => { };
// Contravariance of input: Action<Base> "is a" Action<Derived>.
derivedAction = baseAction;

It is natural if the “Derived -> Base” pair is replaced by “string -> object” pair:

// Derived "is a" Base, 
// while string "is a" object.
// Here Derived is replaced by string,
// and Base is replaced by object.
Action<string> stringAction = arg => { };
Action<object> objectAction = arg => { };
// The same contravariance of input: Action<object> "is a" Action<string>.
stringAction = objectAction;

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

// Derived "is a" Base,
// while Action<Base> "is a" Action<Derived>.
// Here Derived is replaced by Action<Base>,
// and Base is replaced by Action<Derived>.
Action<Action<Base>> baseActionIn = arg => { };
Action<Action<Derived>> derivedActionIn = arg => { };
// The same contravariance of input: Action<Action<Derived>> "is a" Action<Action<Base>>.
baseActionIn = derivedActionIn;

So this is proved: Action<Action<Derived>> "is a" Action<Action<Base>>.

Again, ActionIn<T>(Action<T>) and Action<Action<T>> are the same higher-order function. They both take a Action<T> as a input, and return void. Now we understood the variance: Action<Action<Derived>> "is a" Action<Action<Base>>. That means, ActionIn<Derived> “is a” ActionIn<Base>.

In a word:

Derived “is a” Base ==contravariance==> Action<Base> "is a" Action<Derived> ==contravariance==> Action<Action<Derived>> "is a" Action<Action<Base>>

is

Derived “is a” Base ==covariance==> Action<Action<Derived>> "is a" Action<Action<Base>>.

In other word, for Action<Action<T>>:

  • T is contravariant for Action<T>, naturally Action<T> is contravariant for Action<Action<T>>;
  • T looks “contravariant contravariant” for Action<Action<T>>, that is, covariant for Action<Action<T>>.

That’s why we use “out” for T in [void ActionIn<out T>(Action<T>)], even T has nothing to do with output.

More variances of input

You may guess a “contravariant contravariant contravariant” type parameter is contravariant. Yes, that’s true. And a “contravariant contravariant contravariant contravariant” type parameter is covariant:

// T is input. T is contravariant.
internal delegate void Action<in T>(T t);

// T is input. T is covariant.
internal delegate void ActionIn<out T>(Action<T> action);

// T is input. T is contravariant.
internal delegate void ActionInIn<in T>(ActionIn<T> actionIn);

// T is input. T is covariant.
internal delegate void ActionInInIn<out T>(ActionInIn<T> actionIn);

and so on.

No contravariance of output

However, for output, if a type parameter is “covariant covariant”, it is not contravariant:

// T is covarianct for Func<T>.
internal delegate T Func<out T>();

// T is still covariant for higher-order FuncOut<T>.
internal delegate Func<T> FuncOut<out T>();

internal class Program
{
    private static void Main()
    {
        Func<Base> baseFunc = () => new Base();
        Func<Derived> derivedFunc = () => new Derived();
        // T is covarianct for Func<T>.
        baseFunc = derivedFunc;

        FuncOut<Base> baseFuncOut = () => () => new Base();
        FuncOut<Derived> derivedOut = () => () => new Derived();
        // T is still covariant for higher-order FuncOut<T>.
        baseFuncOut = derivedOut;
    }
}

A “covariant covariant” scenario is still covariant. There is no contravariance of output.

Conclusion

The previous parts tried to impress you that:

  • Covariance is about output;
  • Contravariance is about input.

Now because

  • Covariance of covariance is covariance;
  • Contravariance of contravariance is covariance.

In the special scenario of higher-order function

internal delegate void ActionIn<out T>(Action<T> action);

The input becomes covariant.

Published Monday, August 31, 2009 1:02 PM by Dixin

Comments

No Comments

Leave a Comment

(required) 
(required) 
(optional)
(required)