- Integrated
- Unitive
- Extensible Provider Model
- Declarative: Not How, But What
- Hierarchical
- Composable
- Transformative
- Summary
Declarative: Not How, But What
LINQ is declarative, not imperative. It allows developers to simply state what they want to do without worrying about how it is done.
Imperative programming requires developers to define step by step how code should be executed. To give directions in an imperative fashion, you say, “Go to 1st Street, turn left onto Main, drive two blocks, turn right onto Maple, and stop at the third house on the left.” The declarative version might sound something like this: “Drive to Sue’s house.” One says how to do something; the other says what needs to be done.
The declarative style has two advantages over the imperative style:
- It does not force the traveler to memorize a long set of instructions.
- It allows the traveler to optimize the route when possible.
It should be obvious that there is little opportunity to optimize the first set of instructions for getting to Sue’s house: You simply have to follow them by rote. The second set, however, allows the traveler to use his or her knowledge of the neighborhood to find a shortcut. For instance, a bike might be the best way to travel at rush hour, whereas a car might be best at night. On occasion, going on foot and cutting through the local park might be the best solution.
Here is another example of the difference between declarative and imperative code:
// imperative style List<int> imperativeList = new List<int>(); imperativeList.Add(1); imperativeList.Add(2); imperativeList.Add(3); // declarative style List<int> declaractiveList = new List<int> { 1, 2, 3 };
The first example details exactly how to add items to a list. The second example states what you want to do and allows the compiler to figure out the best way to do it. As you will learn in the next chapter, both styles are valid C# 3.0 syntax. The declarative form of this code, however, is shorter, easier to understand, easier to maintain, and, at least in theory, leaves the compiler free to optimize how a task is performed.
These two styles differ in both the amount of detail they require a developer to master and the amount of freedom that each affords the compiler. Detailed instructions not only place a burden on the developer, but also restrict the compiler’s capability to optimize code.
Let’s consider another example of the imperative style of programming. As developers, we frequently end up in a situation where we are dealing with a list of lists:
List<int> list01 = new List<int> { 1, 2, 3 }; List<int> list02 = new List<int> { 4, 5, 6 }; List<int> list03 = new List<int> { 7, 8, 9 }; List<List<int>> lists = new List<List<int>> { list01, list02, list03 };
Here is imperative code for accessing the members of this list:
List<int> newList = new List<int>(); foreach (var item in lists) { foreach (var number in item) { newList.Add(number); } }
This code produces a single list containing all the data from the three nested lists:
1 2 3 4 5 6 7 8 9
Notice that we have to write nested for loops to allow access to our data. In a simple case like this, nested loops are not terribly complicated to use, but they can become very cumbersome in more complex problem domains.
Contrast this code with the declarative style used in a LINQ program:
var newList = from list in lists from num in list select num;
You can access the results of these two “query techniques” in the same way:
foreach (var item in newList) { Console.WriteLine(item); }
This code writes the results of either query, producing identical results, regardless of whether you used the imperative or declarative technique to query the data:
1 2 3 4 5 6 7 8 9
The difference here is not in the query’s results, or in how we access the results, but in how we compose our query against our nested list. The imperative style can sometimes be verbose and hard to read. The declarative code is usually short and easy to read and scales more easily to complex cases. For instance, you can add an orderby clause to reverse the order of the integers in your result set:
var query = from list in lists from num in list orderby num descending select num;
You probably know how to achieve the same results using the imperative style. But it was knowledge that you had to struggle to learn, and it is knowledge that applies only to working with sequences of numbers stored in a List<T>. The LINQ code for reordering results, however, is easy to understand. It can be used to reorder not only nested collections, but also SQL data, XML data, or the many other data sources we query using LINQ.
To get the even numbers from our nested lists, we need only do this:
var query = from list in lists from num in list where num % 2 == 0 orderby num descending select num;
Contrast this code with the imperative equivalent:
List<int> newList = new List<int>(); foreach (var item in lists) { foreach (var number in item) { if (number % 2 == 0) { newList.Add(number); } } } newList.Reverse();
This imperative style of programming now has an if block nested inside the nested foreach loops. This is not only verbose and applicable to only a specific type of data, it also can be like a straight jacket for both the compiler and the developer. Commands must be issued and followed in a rote fashion, leaving little room for optimizations.
The equivalent LINQ query expression does not describe in a step-by-step fashion how to query our list of lists. It simply lets the developer state what he wants to do and lets the compiler determine the best path to the destination.
After nearly 50 years of steady development, the possibilities inherent in imperative programming have been extensively explored. Innovations in the field are now rare. Declarative programming, on the other hand, offers opportunities for growth. Although it is not a new field of study, it is still rich in possibilities.
Because LINQ is a new technology from Microsoft, you might find it a bit jarring to see me write that declarative programming is not new. In fact, declarative code has been with us nearly as long as imperative code. Some older languages such as LISP (which was first specified in 1958) make heavy use of the declarative style of programming. Haskel and F# are examples of other languages that use it extensively. One reason LINQ and SQL look so much alike is that they are both forms of declarative programming.
The point of LINQ is not that it will replace SQL, but that it will bring the benefits of SQL to C# developers. LINQ is a technology for enabling a SQL-like declarative programming style inside a native C# program. It brings you the benefits of SQL but adds declarative syntax, as well as syntax highlighting, IntelliSense support, type checking, debugging support, the ability to query multiple data sources with the same syntax, and much more.