C# Linq与Currying的比较

16

我正在尝试学习函数式编程及其各种概念,这些都非常有趣。我已经多次阅读过柯里化以及它的优点。

但是我并不理解这个概念的关键所在。下面的示例展示了使用柯里化概念及linq解决问题的过程。实际上,我没有看到使用柯里化概念的任何优势。

那么,使用柯里化的优点是什么呢?

static bool IsPrime(int value)
{
    int max = (value / 2) + 1;
    for (int i = 2; i < max; i++)
    {
        if ((value % i) == 0)
        {
            return false;
        }
    }
    return true;
}

static readonly Func<IEnumerable<int>, IEnumerable<int>> GetPrimes = 
        HigherOrder.GetFilter<int>().Curry()(IsPrime);

static void Main(string[] args)
{
    int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };

    Console.Write("Primes:");
    //Curry
    foreach (int n in GetPrimes(numbers))
    {
        Console.Write(" {0}", n);
    }
    Console.WriteLine();

    //Linq
    foreach (int n in numbers.Where(p => IsPrime(p)))
    {
        Console.Write(" {0}", n);
    }

    Console.ReadLine();
}

这里是高阶过滤器方法:

public static Func<Func<TSource, bool>, IEnumerable<TSource>, IEnumerable<TSource>> GetFilter<TSource>()
{
    return Filter<TSource>;
}

3
除了混淆代码之外,我没有看到任何明显的好处。 - RQDQ
4
我看不到你的示例代码中有任何柯里化。此外,HigherOrder是什么? - leppie
为什么不使用Linq to Curry - CrazyDart
1
@phoog:听起来像是一种毫无意义的做法! - leppie
@leppie,在C#中,我同意你的观点,但是在函数式语言中,这个概念非常有用。 - phoog
显示剩余7条评论
3个回答

41

使用currying的优点是什么?

首先,让我们澄清一些术语。人们使用“currying”来表示两种含义:

  1. 将具有两个参数的方法重新制定为仅具有一个参数并返回一个仅具有一个参数的方法
  2. 对具有两个参数的方法进行部分应用以生成一个仅具有一个参数的方法

显然,这两个任务密切相关,因此会引起混淆。在正式讲话时,应该将“currying”限制为指第一定义,但在非正式场合,任何一种用法都很常见。

所以,如果你有一个方法:

static int Add(int x, int y) { return x + y; }

你可以这样调用它:

int result = Add(2, 3); // 5

你可以对Add方法进行柯里化:

static Func<int, int> MakeAdder(int x) { return y => Add(x, y); }

现在:

Func<int, int> addTwo = MakeAdder(2);
int result = addTwo(3); // 5

局部应用有时在非正式场合下也被称为“柯里化”,因为它显然有关联:

Func<int, int> addTwo = y=>Add(2,y);
int result = addTwo(3);
你可以制造一台机器来执行这个过程:
static Func<B, R> PartiallyApply<A, B, R>(Func<A, B, R> f, A a)
{
    return (B b)=>f(a, b);
}
...
Func<int, int> addTwo = PartiallyApply<int, int, int>(Add, 2);
int result = addTwo(3); // 5

现在我们来回答你的问题:

使用柯里化有哪些优势?

这两种技巧的优点在于它们使得方法的处理更加灵活。

例如,假设你正在编写路径查找算法的实现。你可能已经拥有一个帮助方法,可以给你两个点之间的近似距离:

static double ApproximateDistance(Point p1, Point p2) { ... }

但是,当你实际构建算法时,通常想要知道的是当前位置与一个固定终点之间的距离。算法所需的是Func<Point, double>——即从位置到固定终点的距离。而你拥有的是Func<Point, Point, double>。那么,你要如何将所拥有的转换为所需的?通过部分应用;你将固定终点作为近似距离方法的第一个参数部分应用,然后得到一个函数,该函数可以满足路径查找算法需要消耗的内容:

Func<Point, double> distanceFinder = PartiallyApply<Point, Point, double>(ApproximateDistance, givenEndPoint);

如果一开始ApproximateDistance方法已经柯里化:

static Func<Point, double> MakeApproximateDistanceFinder(Point p1) { ... }

那么你就不需要自己进行部分应用了;你只需要使用固定的终点调用MakeApproximateDistanceFinder,然后就完成了。

Func<Point, double> distanceFinder = MakeApproximateDistanceFinder(givenEndPoint);

Eric,你是否也知道这个概念的名称?其中一个函数返回A并接受B,另一个函数返回B并接受C,因此基于此,可能会有另一个函数接受C并返回A。我不确定我是否解释得正确,但这是一个函数式编程的概念。 - Joan Venge
1
@JoanVenge 这被称为函数合成 - Rodrick Chapman

6

C#中实现柯里化(partial function)有什么优势?的评论中,@Eric Lippert先生指向了这篇博客:

Currying and Partial Function Application

我发现这篇文章给出的最好解释,适用于我:

从理论上讲,它(柯里化)将lambda演算简化为仅包括最多一个参数的函数。从实际应用的角度来看,它允许程序员通过固定前k个参数从基本函数生成函数族。这类似于将一些东西钉在墙上需要两个图钉。在被钉之前,物体可以自由地在表面上移动;但是,在放置第一个大头钉时,就会受到限制。最后,当第二个钉子放置时,就不再有任何运动自由度。同样地,当程序员柯里化一个带有两个参数的函数并将其应用于第一个参数时,功能将被限制为一个维度。最后,当他将新函数应用于第二个参数时,将计算出特定的值。

进一步来说,我认为函数式编程本质上引入了“数据流编程而不是控制流”,这类似于使用SQL而不是C#。有了这个定义,我看到了LINQ的原因,以及它在纯Linq2Objects之外具有许多应用,例如Rx中的事件。


1
使用柯里化的优点主要在于函数式语言中,这些语言是为了从柯里化中受益而构建的,并且具有方便的语法来表达该概念。C#不是这样一种语言,在C#中实现柯里化通常很难理解,就像表达式HigherOrder.GetFilter<int>().Curry()(IsPrime)一样。

3
我认为在 C# 中,人们偶尔仍然需要柯里化,但他们通常会通过手动柯里化特定函数(即编写返回委托的函数)来解决这个需求,而不是使用通用的柯里化函数。 柯里化仍然在使用中,只是不是自动完成的。 - Brian
@Brian 这是一个很好的观点,特别是随着 lambda 表达式的出现。 - phoog

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接