Exploring Lambda Expressions (
Page 1 of 2 )
Paul Kimmel continues his exploration of Lambda Expressions. Understanding Lambda Expressions are important to LINQ and other new technologies.
Introduction
Wikipedia may not be the definitive resource on any one thing. In fact, it may even be possible to use Wikipedia as a propagandist tool. But, Wikipedia is an excellent starting place for learning just about anything. For example, you can learn that Alonzo Church invented Lambda Calculus, and you can read about Schonfinklisation, if you are so inclined. If you want to do a deep dive into Lambda Calculus then Wikipedia is a pretty good starting point. We are going to talk about Lambda Expressions, which is an offshoot of the Lambda Calculus.
Lambda Expressions are based on functional programming invented by Church in the 1930s. C# is not the first language to include Lambda Expressions, but it’s the one we’ll talk about here.
In this article I am going to introduce Lambda Expressions, but I am also going to talk about closures and currying (or Schonfinkelisation). There are other articles about Lambda Expressions on the web and fewer about Closures and fewer still about Currying. I am also going to explore partial completions though I am by no means an expert; I hope it provides you with a good starting point on the latter.
What are Lambda Expressions?
Lambda Expressions are based on the calculus of the same name but more simplistically they can be thought of as very short, condensed functions. Think of them as function-shorthand.
More than thirty years ago something now referred to as a macro was invented. Macros were bits of code that were named and inserted wherever the macro name was present. A pre-compiler performed this operation. An evolution of copy and replace macros was the function. The idea behind a function is one instance of the code and we re-direct instruction to the location in memory where the code is rather than move the code. (When I was writing assembly code and C it was the CS and IP registers in the microprocessor that played that role, as they still do today.)
Once the relationship between an address and a function was understood we sort of invented function pointers. Function pointers led to the event driven model of programming (which ultimately led to interactive operating systems like Windows). Function pointers were deemed a little crude and the delegate was born. A delegate is really just the objectification of the function pointer, but delegates are an improvement over raw function addresses. Delegates led to anonymous delegates which in turn are the precursor to Lambda Expressions.
A Lambda Expression is a function without the modifiers, return type, function keyword, argument types, or return statement. What’s left? The parameters on the left, the Lambda operator (=>, the “gosinta”, as I call it), and the behaviors on the right of the Lambda operator. A Lambda Expression that adds two integers can be written like this:
(x,y) => x + y;
Pretty short.
We can omit the parameter types as shown or include them, if you like. We can also include multiple statements by adding the { } pair (function body operator). (The extra baggage of parameter types and brackets detracts from the simplicity implicit arguments and singular statements, but it can be added.)
Lambda Expressions may be hard to read for many for a while, but we’ll get used to them. And, yes, Lambda Expressions make code harder to read. However, Lambda Expressions exist to support LINQ (Language INtegrated Query), and for that reason alone they are needed.
The brief fragment above demonstrated a simple Lambda Expression (and my other article ; Programming with Generic Delegates and Lambda Expressions provides several more examples. Let’s move on to more advanced subjects.
Closures
Lambda Expressions can reference local variables. Local variables have function scope. The general rule of locally scoped variables is when the scope goes away so too does the local variables. If a Lambda Expression were to refer to a locally scoped variable that was cleaned up with its encapsulating scope, but the Lambda Expression was still around, the expression would be left with a dangling reference. That is, the expression would reference an undefined bit of memory.
To eliminate dangling references Lambda Expressions that refer to local variables create a closure. A closure is nothing more than a compiler generated wrapper class that makes a copy of the locally scoped variable for the benefit of the Lambda Expression. All of this plumbing is transparent to the coder and user and is handled by the compiler. Listing 1 shows an example of a Lambda Expression that will create a closure. (It isn’t necessary to return the Lambda Expression, that is, extend its scope, for the closure to be created.)
Listing 1: A Lambda Expression that is wrapped in a closure due to the reference to the local variable rate.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
namespace LambdaExpressions
{
class Program
{
static void Main(string[] args)
{
// very simple lambda
Action<int> wait = time => System.Threading.Thread.Sleep(time);
// multi-statement lambda
Action<string> pause = message =>
{
Console.WriteLine(message);
wait(500);
};
// closure Lambda
float rate = 0.06f;
Func<float, float> michiganTax = amount => amount * (1 + rate);
pause("Michigan tax (.06): " + michiganTax(99.99f).ToString());
}
}
}
The closure is created to wrap rate and the Lambda Expression amount => amount * (1+rate). If you examine the MSIL with ILDASM (refer to Figure 1) you can see the wrapper class (highlighted).
Figure 1: the generated wrapper class, a closure named c__DisplayClass4, is generated because local variables are referenced by the Lambda Expression.