Is it ever too late to add another answer?
I've written a ton of LINQ-to-objects code and I contend that at least in that domain that it's good to understand both syntaxes in order to use whichever makes for simpler code -- which isn't always dot-syntax.
Of course there are times when dot-syntax IS the way to go - others have provided several of these cases; however, I think comprehensions have been short-changed - given a bad rap, if you will. So I'll provide a sample where I believe comprehensions are useful.
Here's a solution to a digit substitution puzzle:
(solution written using LINQPad, but can stand-alone in a console app)
// NO
// NO
// NO
//+NO
//===
// OK
var solutions =
from O in Enumerable.Range(1, 8) // 1-9
//.AsQueryable()
from N in Enumerable.Range(1, 8) // 1-9
where O != N
let NO = 10 * N + O
let product = 4 * NO
where product < 100
let K = product % 10
where K != O && K != N && product / 10 == O
select new { N, O, K };
foreach(var i in solutions)
{
Console.WriteLine("N = {0}, O = {1}, K = {2}", i.N, i.O, i.K);
}
//Console.WriteLine("\nsolution expression tree\n" + solutions.Expression);
...which outputs:
N = 1, O = 6, K = 4
Not too bad, the logic flows linearly and we can see that it comes up with a single correct solution. This puzzle is easy enough to solve by hand: reasoning that 3 > N
> 0, and O
> 4 * N implies 8 >= O
>= 4. That means there's a maximum of 10 cases to test by hand (2 for N
-by- 5 for O
). I've strayed enough - this puzzle is offered for LINQ illustration purposes.
Compiler Transformations
There's a lot the compiler does to translate this into equivalent dot-syntax. Besides the usual second and subsequent from
clauses get turned into SelectMany
calls we have let
clauses that become Select
calls with projections, both of which use transparent-identifiers. As I am about to show, having to name these identifiers in the dot-syntax takes away from the readability of that approach.
I have a trick for exposing what the compiler does in translating this code to dot syntax. If you uncomment the two commented lines above and run it again, you'll get the following output:
N = 1, O = 6, K = 4
solution expression tree
System.Linq.Enumerable+d_b8.SelectMany(O => Range(1, 8), (O, N) => new <>f_AnonymousType02(O = O, N = N)).Where(<>h__TransparentIdentifier0 => (<>h__TransparentIdentifier0.O != <>h__TransparentIdentifier0.N)).Select(<>h__TransparentIdentifier0 => new <>f__AnonymousType1
2(<>h_TransparentIdentifier0 = <>h_TransparentIdentifier0, NO = ((10 * <>h_TransparentIdentifier0.N) + <>h_TransparentIdentifier0.O))).Select(<>h_TransparentIdentifier1 => new <>f_AnonymousType22(<>h__TransparentIdentifier1 = <>h__TransparentIdentifier1, product = (4 * <>h__TransparentIdentifier1.NO))).Where(<>h__TransparentIdentifier2 => (<>h__TransparentIdentifier2.product < 100)).Select(<>h__TransparentIdentifier2 => new <>f__AnonymousType3
2(<>h_TransparentIdentifier2 = <>h_TransparentIdentifier2, K = (<>h_TransparentIdentifier2.product % 10))).Where(<>h_TransparentIdentifier3 => (((<>h_TransparentIdentifier3.K != <>h_TransparentIdentifier3.<>h_TransparentIdentifier2.<>h_TransparentIdentifier1.<>h_TransparentIdentifier0.O) AndAlso (<>h_TransparentIdentifier3.K != <>h_TransparentIdentifier3.<>h_TransparentIdentifier2.<>h_TransparentIdentifier1.<>h_TransparentIdentifier0.N)) AndAlso ((<>h_TransparentIdentifier3.<>h_TransparentIdentifier2.product / 10) == <>h_TransparentIdentifier3.<>h_TransparentIdentifier2.<>h_TransparentIdentifier1.<>h_TransparentIdentifier0.O))).Select(<>h_TransparentIdentifier3 => new <>f_AnonymousType4`3(N = <>h_TransparentIdentifier3.<>h_TransparentIdentifier2.<>h_TransparentIdentifier1.<>h_TransparentIdentifier0.N, O = <>h_TransparentIdentifier3.<>h_TransparentIdentifier2.<>h_TransparentIdentifier1.<>h_TransparentIdentifier0.O, K = <>h__TransparentIdentifier3.K))
Putting each LINQ operator on a new line, translating the "unspeakable" identifiers to ones we can "speak", changing the anonymous types to their familiar form and changing the AndAlso
expression-tree lingo to &&
exposes the transformations the compiler does to arrive at an equivalent in dot-syntax:
var solutions =
Enumerable.Range(1,8) // from O in Enumerable.Range(1,8)
.SelectMany(O => Enumerable.Range(1, 8), (O, N) => new { O = O, N = N }) // from N in Enumerable.Range(1,8)
.Where(temp0 => temp0.O != temp0.N) // where O != N
.Select(temp0 => new { temp0 = temp0, NO = 10 * temp0.N + temp0.O }) // let NO = 10 * N + O
.Select(temp1 => new { temp1 = temp1, product = 4 * temp1.NO }) // let product = 4 * NO
.Where(temp2 => temp2.product < 100) // where product < 100
.Select(temp2 => new { temp2 = temp2, K = temp2.product % 10 }) // let K = product % 10
.Where(temp3 => temp3.K != temp3.temp2.temp1.temp0.O && temp3.K != temp3.temp2.temp1.temp0.N && temp3.temp2.product / 10 == temp3.temp2.temp1.temp0.O)
// where K != O && K != N && product / 10 == O
.Select(temp3 => new { N = temp3.temp2.temp1.temp0.N, O = temp3.temp2.temp1.temp0.O, K = temp3.K });
// select new { N, O, K };
foreach(var i in solutions)
{
Console.WriteLine("N = {0}, O = {1}, K = {2}", i.N, i.O, i.K);
}
Which if you run you can verify that it again outputs:
N = 1, O = 6, K = 4
...but would you ever write code like this?
I'd wager the answer is NONBHN (Not Only No, But Hell No!) - because it's just too complex. Sure you can come up with some more meaningful identifier names than "temp0" .. "temp3", but the point is that they don't add anything to the code - they don't make the code perform better, they don't make the code read better, they only ugly-up the code, and if you were doing it by hand, no doubt you would mess it up a time or three before getting it right. Also, playing "the name game" is hard enough for meaningful identifiers, so I welcome the break from the name-game that the compiler provides me in query comprehensions.
This puzzle sample may not be real-world enough for you to take seriously; however, other scenarios do exist where query comprehensions shine:
- The complexity of
Join
and GroupJoin
: the scoping of range variables in query comprehension join
clauses turn mistakes that might otherwise compile in dot-syntax into compile-time errors in comprehension syntax.
- Any time the compiler would introduce a transparent-identifier in the comprehension transform, comprehensions become worthwhile. This includes the use of any of the following: multiple
from
clauses, join
& join..into
clauses and let
clauses.
I know of more than one engineering shop in my hometown that has outlawed comprehension syntax. I think this is a pity as comprehension syntax is but a tool and a useful one at that. I think it's a lot like saying, "There are things you can do with a screwdriver that you can't do with a chisel. Because you can use a screwdriver as a chisel, chisels are banned henceforth under decree of the king."