关于函数式编程:F#咖喱函数

关于函数式编程:F#咖喱函数

F# curried function

任何人都有一个体面的例子,最好是实际的/有用的,他们可以发表证明这个概念的例子吗?


(Edit: a small Ocaml FP Koan to start things off)

The Koan of Currying (A koan about food, that is not about food)

A student came to Jacques Garrigue and said,"I do not understand what currying is good for." Jacques replied,"Tell me your favorite meal and your favorite dessert". The puzzled student replied that he liked okonomiyaki and kanten, but while his favorite restaurant served great okonomiyaki, their kanten always gave him a stomach ache the following morning. So Jacques took the student to eat at a restaurant that served okonomiyaki every bit as good as the student's favorite, then took him across town to a shop that made excellent kanten where the student happily applied the remainder of his appetite. The student was sated, but he was not enlightened ... until the next morning when he woke up and his stomach felt fine.


我的示例将涵盖将其用于代码的重用和封装。一旦您看了这些,这是相当明显的,并且应该给您一个具体的,简单的示例,您可以考虑将其应用于多种情况。

我们想在树上绘制地图。如果该函数需要多个参数,则可以对该函数进行咖喱处理并将其应用于每个节点-因为我们将在节点上应用该函数作为它的最终参数。不必着急,但是编写另一个函数(假设该函数在其他实例中与其他变量一起使用)将很浪费。

1
2
3
4
5
6
7
8
type 'a tree = E of 'a | N of 'a * 'a tree * 'a tree
let rec tree_map f tree = match tree with
    | N(x,left,right) -> N(f x, tree_map f left, tree_map f right)
    | E(x) -> E(f x)

let sample_tree = N(1,E(3),E(4)
let multiply x y = x * y
let sample_tree2 = tree_map (multiply 3) sample_tree

但这与以下内容相同:

1
let sample_tree2 = tree_map (fun x -> x * 3) sample_tree

因此,这种简单情况并不令人信服。确实如此,而且一旦您更多地使用该语言,它自然就会遇到这些情况,并且功能强大。另一个示例包含一些重复使用的代码。创建素数的递归关系。那里有很多相似之处:

1
2
3
4
5
6
7
8
9
10
11
let rec f_recurrence f a seed n =
    match n with
    | a -> seed
    | _ -> let prev = f_recurrence f a seed (n-1) in
           prev + (f n prev)

let rowland = f_recurrence gcd 1 7
let cloitre = f_recurrence lcm 1 1

let rowland_prime n = (rowland (n+1)) - (rowland n)
let cloitre_prime n = ((cloitre (n+1))/(cloitre n)) - 1

好的,现在rowland和cloitre是咖喱函数,因为它们具有自由变量,并且我们可以获取其序列的任何索引,而无需了解或担心f_recurrence。


虽然前面的示例回答了这个问题,但下面是两个更简单的示例,说明Currying如何对F#编程有益。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
open System.IO

let appendFile (fileName : string) (text : string) =
    let file = new StreamWriter(fileName, true)
    file.WriteLine(text)
    file.Close()

// Call it normally    
appendFile @"D:\Log.txt""Processing Event X..."

// If you curry the function, you don't need to keep specifying the
// log file name.
let curriedAppendFile = appendFile @"D:\Log.txt"

// Adds data to"Log.txt"
curriedAppendFile"Processing Event Y..."

并且不要忘记,您可以使用Printf系列功能!在咖喱版本中,请注意明显缺少lambda。

1
2
3
4
5
// Non curried, Prints 1 2 3
List.iter (fun i -> printf"%d" i) [1 .. 3];;

// Curried, Prints 1 2 3
List.iter (printfn"%d") [1 .. 3];;


Currying描述了将具有多个参数的函数转换为单参数函数链的过程。 C#中的三个参数函数示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Func<T1, Func<T2, Func<T3, T4>>> Curry<T1, T2, T3, T4>(Func<T1, T2, T3, T4> f)
{
    return a => b => c => f(a, b, c);
}

void UseACurriedFunction()
{
    var curryCompare = Curry<string, string, bool, int>(String.Compare);
    var a ="SomeString";
    var b ="SOMESTRING";
    Console.WriteLine(String.Compare(a, b, true));
    Console.WriteLine(curryCompare(a)(b)(true));

    //partial application
    var compareAWithB = curryCompare(a)(b);
    Console.WriteLine(compareAWithB(true));
    Console.WriteLine(compareAWithB(false));
}

现在,布尔参数可能不是您最希望在部分应用程序中保持打开状态的参数。这就是为什么F#函数中的参数顺序起初看起来有些奇怪的原因之一。让我们定义一个不同的C#咖喱函数:

1
2
3
4
Func<T3, Func<T2, Func<T1, T4>>> BackwardsCurry<T1, T2, T3, T4>(Func<T1, T2, T3, T4> f)
{
    return a => b => c => f(c, b, a);
}

现在,我们可以做一些更有用的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void UseADifferentlyCurriedFunction()
{
    var curryCompare = BackwardsCurry<string, string, bool, int>(String.Compare);

    var caseSensitiveCompare = curryCompare(false);
    var caseInsensitiveCompare = curryCompare(true);

    var format = Curry<string, string, string, string>(String.Format)("Results of comparing {0} with {1}:");

    var strings = new[] {"Hello","HELLO","Greetings","GREETINGS"};

    foreach (var s in strings)
    {
        var caseSensitiveCompareWithS = caseSensitiveCompare(s);
        var caseInsensitiveCompareWithS = caseInsensitiveCompare(s);
        var formatWithS = format(s);

        foreach (var t in strings)
        {
            Console.WriteLine(formatWithS(t));
            Console.WriteLine(caseSensitiveCompareWithS(t));
            Console.WriteLine(caseInsensitiveCompareWithS(t));
        }
    }
}

为什么在C#中使用这些示例?因为在F#中,默认情况下使用函数声明。您通常不需要咖喱函数;他们已经咖喱了。主要的例外是框架方法和其他重载函数,它们采用包含多个参数的元组。因此,您可能希望使用此类函数,实际上,当我在寻找可以做到这一点的库函数时遇到了这个问题。我想它丢失了(如果确实存在),因为实现起来很简单:

1
2
3
4
5
6
7
8
9
10
11
let curry f a b c = f(a, b, c)

//overload resolution failure: there are two overloads with three arguments.
//let curryCompare = curry String.Compare

//This one might be more useful; it works because there's only one 3-argument overload
let backCurry f a b c = f(c, b, a)
let intParse = backCurry Int32.Parse
let intParseCurrentCultureAnyStyle = intParse CultureInfo.CurrentCulture NumberStyles.Any
let myInt = intParseCurrentCultureAnyStyle"23"
let myOtherInt = intParseCurrentCultureAnyStyle"42"

要解决String.Compare的故障,由于据我所知无法指定要选择的3参数重载,因此可以使用非通用解决方案:

1
2
let curryCompare s1 s2 (b:bool) = String.Compare(s1, s2, b)
let backwardsCurryCompare (b:bool) s1 s2 = String.Compare(s1, s2, b)

我不会详细介绍F#中的部分函数应用程序的用法,因为其他答案已经涵盖了这一点。


您知道可以在列表上映射功能吗?例如,映射一个函数以向列表的每个元素添加一个:

1
2
> List.map ((+) 1) [1; 2; 3];;
val it : int list = [2; 3; 4]

实际上,这已经在使用currying了,因为(+)运算符用于创建一个函数以在其参数中添加一个,但是您可以通过更改示例以映射列表的相同函数,从而从该示例中挤出更多内容:

1
2
> List.map (List.map ((+) 1)) [[1; 2]; [3]];;
val it : int list = [[2; 3]; [4]]

如果不进行粗略介绍,您将无法部分应用这些功能,而必须编写如下代码:

1
2
> List.map((fun xs -> List.map((fun n -> n + 1), xs)), [[1; 2]; [3]]);;
val it : int list = [[2; 3]; [4]]

这是一个相当简单的过程。接受一个函数,绑定其参数之一并返回一个新函数。例如:

1
2
let concatStrings left right = left + right
let makeCommandPrompt= appendString"c:\>"

现在,通过使用简单的concatStrings函数,您可以轻松地在任何字符串的前面添加DOS风格的命令提示符!真有用!

好吧,不是真的。我发现一个更有用的情况是,当我想要一个make函数以类似流的方式向我返回数据时。

1
2
let readDWORD array i = array[i] | array[i + 1] << 8 | array[i + 2] << 16 |
    array[i + 3] << 24 //I've actually used this function in Python.

关于它的便利部分是,您不必为自己创建一个完整的类,而无需为它创建整个类,而是调用构造函数,调用obj.readDWORD()。


在我的博客上,我举了一个很好的例子来模拟C#中的currying。要点是,您可以在现有的多参数函数中创建一个针对某个参数封闭的函数(在我的示例中,创建一个针对给定市政当局的价值计算封闭营业税的函数)。

这里吸引人的是,您不必为库克县的销售税而专门创建一个单独的功能,而可以在运行时动态创建(和重用)该功能。


推荐阅读