Fabulous Adventures In Coding

Eric Lippert's Blog

Why does a recursive lambda cause a definite assignment error?

Hey fabulous readers, sorry for not much blogging lately. Between implementing LINQ and making plans to attend my first Burning Man, there's been no time for anything. We've had lots of new ideas generated here for the type inferencing algorithm which I will discuss in detail when I get back.

For now, here's a question I got recently about definite assignment analysis and recursive functions. Clearly this is legal:

delegate void Action<A>(A a);
delegate void Traversal<T>(Tree<T> t, Action<T> a);

static void DepthFirstTraversal<T>(Tree<T> t, Action<T> a){
  if (t == null) return;
  a(t.Value);
  DepthFirstTraversal(t.Left, a);
  DepthFirstTraversal(t.Right, a);
}
static void Traverse<T>(Tree<T> t, Traversal<T> f, Action<T> a){
  f(t, a);
}
static void Print<T>(T t){
  System.Console.WriteLine(t.ToString());
}
static void PrintTree<T>(Tree<T> t){
  Traverse(t, DepthFirstTraversal, Print);
}

What if we wanted that last one to use lambdas?

static void PrintTree<T>(Tree<T> t){
  Traversal<T> df = (t, a)=>{
    if(t==null) return;
    a(t.Value);
    df(t.Left, a);
    df(t.Right, a);
  };
  Traverse(t, df, Print);
}

That doesn't work because it's a definite assignment error. The compiler says that local variable df is being used before it is assigned. You'd have to say

Traversal<T> df = null;
df = (t, a)=>{

And suddenly it would start working.

What the heck? I mean, the original code is a syntactic sugar for:

class Locals{
  Traversal<T> df;
  void M<T>(Tree<T> t, Action<T> a){
    if(t==null) return;
    a(t.Value);
    this.df(t.Left, a);
    this.df(t.Right, a);
  }
}
static void PrintTree<T>(Tree<T> t){
  Locals locals = new Locals();
  locals.df = new Traversal<T>(locals.M<T>);
  Traverse(t, locals.df, Print);
}

And clearly there is no use-before-initialization problem there.

The reason this is an error is because though there is actually no definite assignment problem here, it is not too hard to find a similar situation which does create a problem. Here's a silly example that nevertheless demonstrates a real problem:

delegate D D(D d);
//...
D d = ((D)(c=>c(d))(e=>e);

If you trace out the logic of that thing you'll find that in fact the local variable delegate d is potentially used before it is initialized, and therefore this must be an error.

We could immensely complicate the definite assignment rules by adding rules to discover which lambda and anonymous method bodies are guaranteed to be never called during the initialization, and therefore suppress definite assignment errors on some locals in their bodies. But one of the goals of the standardization process is to make rules that are clear and unambiguous and comprehensible by mortals; enabling recursive anonymous methods isn't a big enough win for the amount of complexity, particularly when you consider that there is the simple workaround of assigning to null and then reassigning.

Published Friday, August 18, 2006 12:09 PM by Eric Lippert
Filed under: ,

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

Duncan said:

Isn't this something that's been solved in Scheme & co. by having letrec?
August 18, 2006 5:42 PM
 

Wes Dyer said:

You can recurse without definite assignment problems but you need to do it with a fixed point combinator.  Here is the change.

using System;

class Program
{
 static void Main()
 {
   var tree = new Tree<int>(3, new Tree<int>(4, new Tree<int>(1), null), new Tree<int>(5));
   PrintTree(tree);
 }

 delegate void Action<A>(A a);
 delegate void Traversal<T>(Tree<T> t, Action<T> a);

 static void Traverse<T>(Tree<T> t, Traversal<T> f, Action<T> a)
 {
   f(t, a);
 }

 static void Print<T>(T t)
 {
   System.Console.WriteLine(t.ToString());
 }

 static void Foo<T>(Tree<T> t, Action<T> a)
 {
   a(t.Value);
 }

 static void PrintTree<T>(Tree<T> tree)
 {
   Traversal<T> df = Fix<T>(f => (t, a) => {
                                             if (t == null) return;
                                             a(t.Value);
                                             f(t.Left, a);
                                             f(t.Right, a);
                                           });
   Traverse(tree, df, Print);
 }

 delegate Traversal<T> TraversalFunctor<T>(Traversal<T> t);
 delegate Traversal<T> Recursive<T>(Recursive<T> r);

 static Traversal<T> Fix<T>(TraversalFunctor<T> f)
 {
   Recursive<T> rec = r => new Traversal<T>((t, a) => f(r(r))(t, a));

   return rec(rec);
 }
}

class Tree<T>
{
 public Tree<T> Left;
 public Tree<T> Right;
 public T Value;

 public Tree(T value) { Value = value; }
 public Tree(T value, Tree<T> left, Tree<T> right) { Value = value; Left = left; Right = right; }
}
August 18, 2006 8:12 PM
 

Adam G said:

For what it's worth, your workaround is more or less what a letrec expands to in Scheme or Common Lisp.

e.g. this form:

(letrec ((len (lambda (ls) (if (null? ls) 0 (+ 1 (len (cdr ls))))))) (len '(1 2 3)))

generates more or less the same code as:

(let ((len '*unassigned*)) (begin (set! len (lambda (ls) ...)) (len '(1 2 3))))

(where '*unassigned* is some special internal value).
August 24, 2006 10:56 AM
 

Charlie Calvert's Community Blog said:

The C# team is happy to welcome you to the new look of the C# Developer Center. This is the home page...
September 4, 2006 9:01 PM
 

Yet Another Language Geek said:

Recursion is beautiful and lambdas are the ultimate abstraction. But how can they be used together? Lambdas

February 2, 2007 2:17 PM

Leave a Comment

(required) 
(optional)
(required) 
Submit

This Blog

Syndication


© 2008 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker