使用C#进行尾递归和memoization

时间:2012-09-28 21:35:26

标签: c# .net tail-recursion memoization

我正在编写一个函数,它根据条目的数据库表查找目录的完整路径。每条记录都包含一个密钥,目录的名称和父目录的密钥(如果您熟悉,它是MSI中的目录表)。我有一个迭代的解决方案,但它开始看起来有点讨厌。我以为我可以编写一个优雅的尾递归解决方案,但我不确定了。

我会告诉你我的代码,然后解释我面临的问题。

Dictionary<string, string> m_directoryKeyToFullPathDictionary = new Dictionary<string, string>();
...
private string ExpandDirectoryKey(Database database, string directoryKey)
{
    // check for terminating condition
    string fullPath;
    if (m_directoryKeyToFullPathDictionary.TryGetValue(directoryKey, out fullPath))
    {
        return fullPath;
    }

    // inductive step
    Record record = ExecuteQuery(database, "SELECT DefaultDir, Directory_Parent FROM Directory where Directory.Directory='{0}'", directoryKey);
    // null check
    string directoryName = record.GetString("DefaultDir");
    string parentDirectoryKey = record.GetString("Directory_Parent");

    return Path.Combine(ExpandDirectoryKey(database, parentDirectoryKey), directoryName);
}

当我意识到我遇到了问题(删除了一些小的验证/按摩)时,这就是代码的样子。我希望尽可能使用memoization来进行短路,但是这需要我对字典进行函数调用以存储递归ExpandDirectoryKey调用的输出。我意识到我在那里也有一个Path.Combine电话,但我认为这可以用... + Path.DirectorySeparatorChar + ...来规避。

我考虑使用一个帮助方法来记忆目录并返回值,这样我就可以在上面函数的末尾像这样调用它:

return MemoizeHelper(
    m_directoryKeyToFullPathDictionary,
    Path.Combine(ExpandDirectoryKey(database, parentDirectoryKey)),
    directoryName);

但我觉得这是作弊而不会被优化为尾递归。

有什么想法吗?我应该使用完全不同的策略吗?这根本不需要是一个超级高效的算法,我真的很好奇。我正在使用.NET 4.0,顺便说一句。

谢谢!

P.S。如果您对我的终止条件感到疑惑,请不要担心。我使用根目录预先编写了字典。

2 个答案:

答案 0 :(得分:2)

即使你确实得到了正确的代码布局,你将遇到的第一个主要问题是C#编译器并不能真正支持尾递归。 Tail recursion in C#

在那里使用信息,你可以使它工作。但它充其量只是不优雅。

答案 1 :(得分:2)

你说你很好奇,我也是,让我们看看你对这种方法的看法。我正在概括你的问题以解释我的观点。我们假设您有Record类型:

class Record
{
    public int Id { get; private set; }
    public int ParentId { get; private set; }
    public string Name { get; private set; }

    public Record(int id, int parentId, string name)
    {
        Id = id;
        ParentId = parentId;
        Name = name;
    }
}

和此函数代表的“数据库”:

static Func<int, Record> Database()
{
    var database = new Dictionary<int, Record>()
     {
         { 1, new Record(1, 0, "a") },
         { 2, new Record(2, 1, "b") },
         { 3, new Record(3, 2, "c") },
         { 4, new Record(4, 3, "d") },
         { 5, new Record(5, 4, "e") },
     };
     return x => database[x];
 }

让我们介绍一种“代表”递归的方法:

public static class Recursor
{
    public static IEnumerable<T> Do<T>(
        T seed, 
        Func<T, T> next, 
        Func<T, bool> exit)
    {
        return Do(seed, next, exit, x => x);
    }

    public static IEnumerable<TR> Do<T, TR>(
        T seed, 
        Func<T, T> next, 
        Func<T, bool> exit, 
        Func<T, TR> resultor)
    {
        var r = seed;
        while (true)
        {
            if (exit(r))
                yield break;
            yield return resultor(r);
            r = next(r);
        }
    }
}

正如您所看到的,这根本不是递归,而是允许我们根据“看起来像”递归的部分来表达算法。我们的路径生成函数如下所示:

static Func<int, string> PathGenerator(Func<int, Record> database)
{
    var finder = database;

    return id => string.Join("/",
        Recursor.Do(id,                       //seed
                    x => finder(x).ParentId,  //how do I find the next item?
                    x => x == 0,              //when do I stop?
                    x => finder(x).Name)      //what do I extract from next item?
                .Reverse());                  //reversing items
}

此函数使用Recursor来表示算法,并收集所有已生成的部分以组成路径。这个想法是Recursor不会累积,它将累积的责任委托给调用者。我们将这样使用它:

var f = PathGenerator(Database());
Console.WriteLine(f(3));           //output: a/b/c

你也想要记忆,让我们来介绍一下:

public static class Memoizer
{
    public static Func<T, TR> Do<T, TR>(Func<T, TR> gen)
    {
        var mem = new Dictionary<T, TR>();
        return (target) =>
        {
            if (mem.ContainsKey(target))
                return mem[target];
            mem.Add(target, gen(target));
            return mem[target];
        };
    }
}

有了这个,您可以记住您对昂贵资源(数据库)的所有访问,只需更改PathGenerator中的一行,即可:

var finder = database;

到此:

var finder = Memoizer.Do<int, Record>(x => database(x));