Skip to content

TypedLambdas

David Arno edited this page Jun 27, 2016 · 1 revision

Typed Lambdas

Succinc<T> typed lambdas guide


What are typed lambdas and why are they needed?

The C# compiler's ability to imply types is ... a little odd. The rules around it are complex, and - to be honest - I'd hesitate to claim I fully understand them. However, they can be summed up reasonably accurately as:

  1. For a variable declaration, if the type of expression after the = is fully deducible, it will imply the type of the variable.
  2. For a lambda, the types of the parameters is never implied from their use in the lambda body. However, the return value will be implied from the body if possible.
  3. If the lambda is supplied as a parameter to a generic method, then, as long as that method's generic parameters are explicitly stated, then the compiler will infer the types of the lambda's parameters.
  4. Conversely, if the types of the parameters of the lambda are explicitly stated, then the return type, and the method's generic parameter types can be inferred.
  5. For method groups (and this includes "groups" with only one method), the compiler downright refuses to infer anything. If supplied as a parameter to a generic method, then that method's generic parameters' types must be explicitly declared. However, when explicitly declared like this, the compiler will select the correct method from the method group to assign to the method.

Points 3 - 5 can be used to take lambdas and method groups and transform them into a fully deducible value, allowing var to be used. This is demonstrated with the following code:

public static Func<T, T, T> Lambda<T>(Func<T, T, T> f) => f;

public static double Sum(double x, double y) => x + y;
 
public void Foo()
{
    var multiply = Lambda<double>((x, y) => x * y);
    var sum = Lambda<double>(Sum);
    var result1 = multiply(2, 3); // result == 6
    var result2 = sum(2, 3); // result == 5
}

By passing both the lambda and method (group) through the Lambda method, they are transformed into fully-typed Func<double, double, double> types and so var can be used to declare a variable that holds these function values.

Succinc<T> offers a range of such methods to allow lambdas and method (bodies) to be typed in this way.

Typed Func lambdas

Lambda

The Lambda methods can be used to type functions that take 1 - 10 parameters of one type and that have the same return type. Because there is only one type involved, each overload only has one type parameter, avoiding the need to explicitly type the parameters. So the following three lines of code are equivalent:

Func<int, int, int, int, int, int> x = (a, b, c, d, e) => a + b + c + d + e;
var y = Lambda((int a, int b, int c, int d, int e) => a + b + c + d + e);
var z = Lambda<int>((a, b, c, d, e) => a + b + c + d + e);

Transform

The Transform methods can be used to type functions that take 1 - 10 parameters of one type, but that return a different type, ie they transform the inputs into a output of a new type. Because there are two types involved, we can take advantage of the compiler's ability to infer the return type when only using one or two parameters. When many parameters are involved though, the type parameters can be supplied to simplify the expression:

var x = Transform((int a) => a == 0 ? Maybe<int>.None() : Maybe<int>.Some(a));
var y = Transform<int, string>((a, b, c, d, e) => $"{a}-{b}-{c}-{d}-{e}");

Func

The Func methods can be used to type functions that take 1 - 10 parameters of more than one type. Just like with Transform we can either specify the types of the parameters via the generic type parameters or the lambda's parameters, but because the return type can be inferred, it's normally shorter to use the latter approach:

var x = Func((int a, double b) => a < 0 ? b * -1 : b);
var y = <int, double, double>((a, b) => a < 0 ? b * -1 : b));

Typed Action lambdas

Lambda

The Lambda methods can be used to type actions that take 1 - 10 parameters of one type. Because there is only one type involved, each overload only has one type parameter, avoiding the need to explicitly type the parameters. So the following three lines of code are equivalent:

Action<int, int, int, int, int, int> x = (a, b, c, d, e) => 
    { WriteLine($"{a + b + c + d + e}"; };
var y = Lambda((int a, int b, int c, int d, int e) =>
    { WriteLine($"{a + b + c + d + e}"; });
var z = Lambda<int>((a, b, c, d, e) => { WriteLine($"{a + b + c + d + e}"; });

Note that {} is used for the lambda bodies in the above example. Succinc<T> uses the name Lambda for methods that type both Func lambdas and Action lambdas. This seems to cause the compiler problems at times though in that favours treating a lambda without a return as a badly behaved Func, rather than an Action. Using {} switches this to it favouring Action and so y and z end up typed correctly.

Action

The Action methods can be used to type actions that take 1 - 10 parameters of more than one type. Just like with Transform and Func we can either specify the types of the parameters via the generic type parameters or the lambda's parameters. Because there is no return type for actions, it's very much a case of personal preference as to which approach to take:

var x = Action((int a, double b) => WriteLine($"{a < 0 ? b * -1 : b}"));
var y = Action<int, double>((a, b) => WriteLine($"{a < 0 ? b * -1 : b}"));
You can’t perform that action at this time.