Don’t Misuse Lambdas
Avoid Duplicating Code
It’s great that so many C# and VB.NET developers are taking advantage of LINQ. Unfortunately, using LINQ can encourage you to misuse lambdas. Consider the following simple example:
var results = from x in 0.Through(10)
select x * x;
Square is a useful utility function. It shouldn’t be defined as a lambda because that ensures code duplication instead of reuse. It’s more obvious using extension method-syntax:
var results =
0.Through(10)
.Select(x => x * x);
The problem is that in versions of C# prior to 4.0, it is painful to define Square as a reusable method because Select is a function with multiple generic parameters. You are forced to painfully specify the type parameters or use an unnecessary lambda:
//specifying the generic types
var results =
0.Through(10)
.Select<int, int>(Math.Square);
//using an unnecessary lambda
var results =
0.Through(10)
.Select(x => Math.Square(x));
Even in C# 4.0, the query syntax retains the unnecessary lambda problem:
var results = from x in 0.Through(10)
select Math.Square(x);
Fortunately, the extension method syntax is fixed in 4.0:
var results =
0.Through(10)
.Select(Math.Square);
Avoid Multi-Line Lambdas
It’s easy to recognize that Square should not be a lambda, but some functions are less obvious:
IEnumerable<Ninja> MakeFearsomeFightingTeam(SecretOoze ooze,
IEnumerable<Turtle> turtles,
Pizza pizza)
{
return
turtles
.Select(turtle =>
{
var transformed = ooze.Transorm(turtle);
transformed.Say("Cowabunga!");
transformed.Eat(pizza);
return transformed;
});
}
You should avoid writing multi-line lambdas like this where possible. In this case, it makes the code harder to read, especially if you add another operation after the Select. It’s difficult to understand what the lambda is doing upon first glance. Also, there’s a good chance that you will want to reuse the behavior of the lambda elsewhere in your program.
IEnumerable<Ninja> MakeFearsomeFightingTeam2(SecretOoze ooze,
IEnumerable<Turtle> turtles,
Pizza pizza)
{
return
turtles
.Select(turtle => Ninjaify(ooze, pizza, turtle));
}
Ninja Ninjaify(SecretOoze ooze, Pizza pizza, Turtle turtle)
{
var transformed = ooze.Transorm(turtle);
transformed.Say("Cowabunga!");
transformed.Eat(pizza);
return transformed;
}
The named instance method gives a description to the behavior that was in the multi-line lambda, and the Select statement is more readable. The sacrifice is that Ninjaify has to take extra arguments because it cannot rely on closure. There is also no guarantee that the definition of Ninjaify will remain close to where it is used as you add more code. Additionally, C#’s syntax requires that you use an unnecessary lambda to pass the turtle argument to Ninjaify. The answer is to use a locally defined method:
IEnumerable<Ninja> MakeFearsomeFightingTeam3(SecretOoze ooze,
IEnumerable<Turtle> turtles,
Pizza pizza)
{
Func<Turtle, Ninja> Ninjaify =
turtle =>
{
var transformed = ooze.Transorm(turtle);
transformed.Say("Cowabunga!");
transformed.Eat(pizza);
return transformed;
};
return
turtles
.Select(Ninjaify);
}
Like a lambda, the locally defined method uses closure to avoid redefining variables, and it has the readability benefit of being defined near to where it is used. Since it is a named method, it provides a useful description of it’s behavior, and it doesn’t disrupt the flow of your Select method. It’s the best of both worlds. The only downside is that it can’t be called outside the scope of the MakeFearsomeFightingTeam3 method.
Unfortunately, C# requires you to write the full type signature for the function. The compiler will fail with a “cannot assign lambda expression to implicitly-typed local variable” if you try to use the var keyword. This is a real pain for more complicated signatures, and it limits you from using locally defined methods to their full potential.
Going Further With F#
In F#, the type signature problem goes away:
let MakeFearsomeFightingTeam4 ooze turtles pizza =
let ninjaify turtle =
let transformed = ooze.Transform(turtle)
transformed.Say("Cowabunga!")
transformed.Eat(pizza)
transformed
turtles
|> Seq.map ninjaify
Here, you can see the benefit of F#’s type inference system and functional programming roots. The behavior of the code is retained, but the extraneous type signature is not required.
F# also fixes the problem of requiring an unnecessary lambda for named methods outside the scope of the MakeFearsomeFightingTeam4 method. Automatic currying makes it easy to pass the arguments that would be captured by closure:
let ninjaify ooze pizza turtle =
let transformed = ooze.Transform(turtle)
transformed.Say("Cowabunga!")
transformed.Eat(pizza)
transformed
let MakeFearsomeFightingTeam5 ooze turtles pizza =
turtles
|> Seq.map (Ninjaify ooze pizza)
In Summary
- Use lambdas for one line, single use functions
- Prefer locally defined functions to multi-line lambdas, but be wary of complicated type signatures in C#
- If a function is reusable, move it to the appropriate class or module, and use it instead of a lambda or locally defined function
- Use F# to avoid messy type signatures and unnecessary lambdas