Using Closures in C#






Using Closures in C#

Problem

You want to associate a small amount of state with some behavior without going to the trouble of building a new class.

Solution

Use anonymous methods to implement closures. Closures can be defined as functions that capture the state of the environment that is in scope where they are declared. Put more simply, they are current state plus some behavior that can read and modify that state. Anonymous methods have the capacity to capture external variables and extend their lifetime, which makes closures possible in C# now.

To show an example of this, you will build a quick reporting system that tracks sales personnel and their revenue production versus commissions. The closure behavior is that you can build one bit of code that does the commission calculations per quarter and works on every salesperson.

First, you have to define your sales personnel:

	class SalesWeasel
	{
	    …
	    #region Private members
	    private string _name;
	    private decimal _annualQuota;
	    private decimal _commissionRate;
	    private decimal _commission = 0m;
	    private decimal _totalCommission = 0m;
	    #endregion // Private members
	    …
	}

Sales personnel have a name, an annual quota, a commission rate for sales, and some storage for holding a quarterly commission and a total commission. Now that you have something to work with, let's write a bit of code to do the work of calculating the commissions:

	delegate void CalculateEarnings(SalesWeasel weasel);

	static CalculateEarnings GetEarningsCalculator(decimal quarterlySales,
	                                   decimal bonusRate,
	                                    int weaselCount)
	    {
	        return delegate(SalesWeasel weasel)
	        {
	            // Assume all weasels contributed equally to quarterly revenue.
	            decimal weaselSalesPortion = quarterlySales / weaselCount;
	            // Figure out the weasel's quota for the quarter.
	            decimal quarterlyQuota = (weasel.AnnualQuota / 4);
	            // Did he make quota for the quarter?
	            if (quarterlySales < quarterlyQuota)
	            {
	                // Didn't make quota, no commission
	                weasel.Commission = 0;
	            }
	            // Check for bonus-level performance (200% of quota).
	            else if (quarterlySales > (quarterlyQuota * 2.0m))
	            {
	             decimal baseCommission = quarterlyQuota *
	            weasel.CommissionRate;
	                weasel.Commission = (baseCommission +
	                        ((quarterlySales - quarterlyQuota) *
	                        (weasel.CommissionRate * (1 + bonusRate))));
	            }
	            else // Just regular commission
	            {
	                weasel.Commission = weasel.CommissionRate * quarterlySales;
	            }
	        };
	    }

You've declared the delegate type as CalculateEarnings, and it takes a SalesWeasel. You have a factory method to construct an instance of this delegate for you called GetEarningsCalculator, which creates an anonymous method to do the calculation of the SalesWeasel's commission and returns a CalculateEarnings instantiation.

To get set up, you have to create your SalesWeasels:

	                  // Set up the sales weasels…
	                  SalesWeasel[] weasels = new SalesWeasel[3];
	                  weasels[0] = new SalesWeasel("Chas",100000m, 0.10m);
	                  weasels[1] = new SalesWeasel("Ray",200000m, 0.025m);
	                  weasels[2] = new SalesWeasel("Biff",50000m, 0.001m);

Then set up earnings calculators based on quarterly earnings:

	                 decimal q1Earnings = 65000m;
	                 decimal q2Earnings = 20000m;
	                 decimal q3Earnings = 37000m;
	                 decimal q4Earnings = 110000m;

	                 // Set up earnings calculators for each quarter.
	                 CalculateEarnings eCalcQ1 =
	                    GetEarningsCalculator(q1Earnings, 0.10m, weasels.Length);
	                CalculateEarnings eCalcQ2 =
	                    GetEarningsCalculator(q2Earnings, 0.10m, weasels.Length);
	                CalculateEarnings eCalcQ3 =
	                    GetEarningsCalculator(q3Earnings, 0.10m, weasels.Length);
	                CalculateEarnings eCalcQ4 =
	                    GetEarningsCalculator(q4Earnings, 0.15m, weasels.Length);

And finally run the numbers for each quarter for all SalesWeasels:

	                 // Figure out Q1.
	                 WriteQuarterlyReport("Q1", q1Earnings, eCalcQ1, weasels);
	                 // Figure out Q2.
	                 WriteQuarterlyReport("Q2", q2Earnings, eCalcQ2, weasels);
	                 // Figure out Q3.
	                 WriteQuarterlyReport("Q3", q3Earnings, eCalcQ3, weasels);
	                 // Figure out Q4.
	                 WriteQuarterlyReport("Q4", q4Earnings, eCalcQ4, weasels);

WriteQuarterlyReport invokes the CalculateEarnings anonymous method implementation (eCalc) for every SalesWeasel and modifies the state to assign quarterly commission values based on the commission rates for each one:

	static void WriteQuarterlyReport(string quarter,
	                            decimal quarterlySales,
	                            CalculateEarnings eCalc,
	                            SalesWeasel[] weasels)
	{
	    Console.WriteLine("{0} Sales Earnings on Quarterly Sales of {1}:",
	        quarter, quarterlySales.ToString("C"));
	    foreach (SalesWeasel weasel in weasels)
	    {
	        // Calc commission
	        eCalc(weasel);
	        // Report
	        Console.WriteLine("     SalesWeasel {0} made a commission of : {1}",

	            weasel.Name, weasel.Commission.ToString("C"));
	    }
	}

You can finally generate the annual report from this data, which will tell the executives which sales personnel are worth keeping by calling WriteCommissionReport:

	            decimal annualEarnings = q1Earnings + q2Earnings +
	                                 q3Earnings + q4Earnings;
	            // Let's see who is worth keeping…
	            WriteCommissionReport(annualEarnings,weasels);

WriteCommissionReport checks the revenue earned by the individual sales personnel against their commission, and if their commission is more than 20 percent of the revenue they generated, you recommend action be taken:

	static void WriteCommissionReport(decimal annualEarnings,
	                            SalesWeasel[] weasels)

	{
	    decimal revenueProduced = ((annualEarnings) / weasels.Length);
	    Console.WriteLine("");
	    Console.WriteLine("Annual Earnings were {0}",
	        annualEarnings.ToString("C"));
	    Console.WriteLine("");
	    foreach(SalesWeasel weasel in weasels)
	    {
	        Console.WriteLine(" Paid {0} {1} to produce {2}",
	            weasel.Name,
	            weasel.TotalCommission.ToString("C"),
	            revenueProduced.ToString("C"));

	        // If his commission is more than 20% of what he produced
	        // can him.
	        if ((revenueProduced * 0.2m) < weasel.TotalCommission)
	        {
	            Console.WriteLine("             FIRE {0}!",weasel.Name);
	        }
	    }
	}

The output for your revenue and commission tracking program is listed here for your enjoyment:

	Q1 Sales Earnings on Quarterly Sales of $65,000.00:
	         SalesWeasel Chas made a commission of : $6,900.00
	         SalesWeasel Ray made a commission of : $1,625.00
	         SalesWeasel Biff made a commission of : $70.25
	Q2 Sales Earnings on Quarterly Sales of $20,000.00:
	         SalesWeasel Chas made a commission of : $0.00
	         SalesWeasel Ray made a commission of : $0.00
	         SalesWeasel Biff made a commission of : $20.00
	Q3 Sales Earnings on Quarterly Sales of $37,000.00:
	         SalesWeasel Chas made a commission of : $3,700.00
	         SalesWeasel Ray made a commission of : $0.00
	         SalesWeasel Biff made a commission of : $39.45
	Q4 Sales Earnings on Quarterly Sales of $110,000.00:
	         SalesWeasel Chas made a commission of : $12,275.00
	         SalesWeasel Ray made a commission of : $2,975.00
	         SalesWeasel Biff made a commission of : $124.63

	Annual Earnings were $232,000.00
	    
	    Paid Chas $22,875.00 to produce $77,333.33
	        FIRE Chas!
	    Paid Ray $4,600.00 to produce $77,333.33
	    Paid Biff $254.33 to produce $77,333.33

Discussion

One of the best ways we've heard of to describe closures in C# is to think of an object as a set of methods associated with data and to think of a closure as a set of data associated with a function. If you need to have several different operations on the same data, an object approach may make more sense. These are two different angles on the same problem, and the type of problem you are solving will help you decide which is the right approach. It just depends on your inclination as to which way to go. There are times when 100 percent pure object-oriented programming can get tedious and unnecessary, and closures are a nice way to solve some of those problems. The SalesWeasel commission example presented here is a demonstration of what you can do with closures. It could have been done without them, but at the expense of writing more class and method code.

Closures have been defined as stated earlier, but there is a stricter definition that essentially implies that the behavior associated with the state should not be able to modify the state in order to be a true closure. We tend to agree more with the first definition as it defines what a closure should be, not how it should be implemented, which seems too restrictive. Whether you choose to think of this as a neat side feature of anonymous methods or you feel it is worthy of being called a closure, it is another programming trick for your toolbox and should not be dismissed.

See Also

See Recipe 9.12; see the "Anonymous Methods" topic in the MSDN documentation.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows