Let me try again a bit more slowly this time, dissecting a simple program that elaborates on the code==data theme in both Scheme and C# 3.0.
// Line 1: (define f (lambda (a) (+ a 1)))
Func<int, int> f = a => a + 1;
// Line 2: (define e (quote (lambda (a) (+ a 1))))
Expression<Func<int, int>> e = a => a + 1;
// Line 3: (define elong (list 'lambda (list 'a ) (list '+ 'a 1 )))
ParameterExpression ap = Expression.Parameter(typeof(int), "a");
Expression<Func<int, int>> elong =
Expression.Lambda<Func<int, int>>(
Expression.Add(ap, Expression.Constant(1)),
ap);
// Line 4: (display (car (car (cddr e)))) ; prints +
Console.WriteLine(e.Body.NodeType); // prints Add
// Line 5:(display (elong 4)) ; runtime error: procedure application: expected procedure, given: (lambda (a) (+ a 1))
Console.WriteLine(elong(4)); // compile-time error CS0118: 'elong' is a 'variable' but is used like a 'method'
// Line 6: (define f2 (eval elong (scheme-report-environment 5)))
Func<int, int> f2 = elong.Compile();
// Line 7: (display (f2 4)) ; prints 5
Console.WriteLine(f2(4)); // prints 5
The first two lines are the most important.
In both Lisp/Scheme and C# 3.0, there's a "quoting" mechanism that tells the front end to leave your expression as data and not turn it into some opaque yet directly executable function.
In Lisp/Scheme, you use the quote special form to turn off the evaluator. The quote special form is often abbreviated with an apostrophe like in my previous post).
In C#, the compiler looks at the context in which a lambda expression is being used. If it's being assigned to a delegate type (like Func), then the compiler emits an opaque IL-based method just like it did in C# 2.0. If it's being assigned to an Expression where TSignature is a delegate type, then it produces an expression tree that is equivalent to the one I later construct by hand and assign to the variable elong (see Line 3). For completeness, I show how to use Scheme's list function to do the equivalent list construction in a more verbose manner (I decided folks would freak out if I did it all with cons).
Note that in Line 4 of our program, we can use the standard mechanisms for traversing structured data to extract any part of the expression. In the Scheme version, I'm using the list destructuring operators car and cddr. In the C# version, I'm using the named property mechanism (LINQ expressions are structured as types with named properties rather than as raw lists). In both the C# and Scheme versions the values of e and elong are equivalent to one another.
Line 5 is the one that trips up a lot of people coming to Lisp or Scheme (or LINQ).
When you quote an expression, you wind up with plain old data that you can slice and dice to your heart's content. What you can't do with data is directly execute it. For that reason, Line 5 is an error that is at compile-time in C# and at runtime in Scheme.
To translate the data into something you can execute, both Lisp/Scheme and C# require you to take an explicit step (shown in Line 6). In Scheme, it's the evaluation of the lambda special form that translates the rest of the list into something executable (a procedure object in R5RS-speak). In LINQ, it's calling the Compile() method on the lambda expression. Both of these operations yield an opaque value that is ready for beta reduction/direct execution. Armed with this opaque executable value, we're off to the races in Line 7 printing out our beloved "5"
Both Patrick and James Robertson allude to the fact that we're marching headlong into the past with this stuff.
If only I could wish away a lot of the stuff that distracted/stunted the industry back in the late 80's and 1990's, but I can't.
Posted
May 12 2006, 05:52 AM
by
don-box