柯里化和部分应用是另外两个直接来自旧数学论文的函数式概念。前者与印度食物毫无关系,尽管它确实很美味,但它以杰出的美国数学家 Haskell Brooks Curry 的名字命名,有不少于三种编程语言以他的名字命名。
柯里化源自 Haskell Curry 在组合逻辑方面的工作,它是现代函数式编程的基础之一。我不会给出枯燥的正式定义,而是通过示例进行解释。这是一个类似于 C# 的 add 函数伪代码:
public interface ICurriedFunctions
{
decimal Add(decimal a, decimal b);
}
var curry = // some logic for obtaining an implementation of the interface
var answer = curry.Add(100, 200);
在这个例子中,我们期望答案是 300(即 100+200),事实确实如此。
但是,如果我只提供一个参数会怎样?像这样:
public interface ICurriedFunctions
{
decimal Add(decimal a, decimal b);
}
var curry = // some logic for obtaining an implementation of the interface
var answer = curry.Add(100); // What could it be?
在这种情况下,如果这是一个假设的柯里化函数,你认为你会在 answer 中返回什么?
我在函数式编程中设计了一个经验法则 - 如果有一个问题,答案很可能是“函数”。这里就是这种情况。
如果这是一个柯里化函数,那么 answer 变量将是一个函数。它将是原始 Add
函数的修改版本,但第一个参数现在固定为值 100 - 有效地使其成为一个新函数,它将 100 添加到您提供的任何内容中。
您可以像这样使用它:
public interface ICurriedFunctions
{
decimal Add(decimal a, decimal b);
}
var curry = // some logic for obtaining an implementation of the interface
var add100 = curry.Add(100); // Func<decimal,decimal>, adds 100 to the input
var answerA = add100(200); // 300 -> 200+100
var answerB = add100(0); // 100 -> 0+100
var answerC = add100(900); // 1000 -> 900+100
它基本上是一种从具有多个参数的函数开始的方法,并从中创建该函数的多个更具体的版本。一个单一的基本函数可以变成许多不同的函数,这与 OO 的继承概念不同。
但是,柯里化的意义究竟是什么?如何使用它?
让我解释一下……
柯里化和大型函数
在我上面给出的“添加”示例中,我们只有一对参数,因此当柯里化可用时,我们只能用它们做两种可能的事情:
-
提供第一个参数,返回一个函数
-
提供两个参数并返回一个值
柯里化如何处理具有超过 2 个基本参数的函数?为此,我将使用一个简单的 CSV 解析器的示例 - 即获取 CSV 文本文件,将其按行分成记录,然后使用一些分隔符(通常是逗号)将其再次分成记录中的单个属性。
假设我编写了一个解析器函数来加载一批书籍数据:
// Input in the format:
//
//title,author,publicationDate
//The Hitch-Hiker's Guide to the Galaxy,Douglas Adams,1979
//Dimension of Miracles,Robert Sheckley,1968
//The Stainless Steel Rat,Harry Harrison,1957
//The Unorthodox Engineers,Colin Kapp,1979
public IEnumerable<Book> ParseBooks(string fileName) =>
File.ReadAllText(fileName)
.Split("\r\n")
.Skip(1) // Skip the header
.Select(x => x.split(",").ToArray())
.Select(x => new Book
{
Title = x[0],
Author = x[1],
PublicationDate = x[2]
});
var bookData = parseBooks(true, Environment.NewLine, ",", "books.csv");
这一切都很好,只是接下来的两组书的格式不同。Books2.csv 使用竖线而不是逗号来分隔字段,Books3.csv 来自 Linux 环境,行尾为“\n”,而不是 Windows 样式的“\r\n”。
我们可以通过创建 3 个彼此几乎相同的不同函数来解决这个问题。但我并不热衷于不必要的重复,因为它会给想要维护代码库的未来开发人员带来太多问题。
更合理的解决方案是为可能发生变化的所有内容添加参数。像这样:
public IEnumerable<Book> ParseBooks(
string lineBreak,
bool skipHeader,
string fieldDelimiter,
string fileName
) =>
File.ReadAllText(fileName)
.Split(lineBreak)
.Skip(skipHeader ? 1 : 0)
.Select(x => x.split(fieldDelimiter).ToArray())
.Select(x => new Book
{
Title = x[