原文出处: http://lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!169.entry
Translated by Mike
我本想多写一些关于柯里化的内容,但是我发现柯里化,元组,函数类型,lambda和partial application都是相关的话题。在我给出特别丰富的实例之前我至少先简单介绍一下它们在F#中是如何工作的。所以今天的文章将是关于这些话题在F#中的基础。
元组
元组是一个很好的开始点。一个元组就是两个或更多值(比如:两个,三个,等等)的一个组合。在F#中它们是一些被逗号分开的实体。下面是一些关于元组的例子:
// t2 : int * string
let t2 = (3, "foo")
// t3 : int * string * bool
let t3 = (4, "bar", true)
针对每一个例子,我已经在它们上面的注释中显示给出了类型注释。这里的“t2”是一个包含int和string的二元组。它的类型名称是“int*string”;各元类型在F#中用“*”连接。同样的,“t3”是一个包含一个布尔型的三元组。
元组外面的一对小括号是可选的,也就是说,我也可以这样来写
let t2 = 3, "foo"
然而为了清晰起见,人们通常还是会加上小括号的,而且我也会鼓励你这么写。(我发现就我个人而言,如果没有小括号的话,逗号很容易被忽视。)
unit类型
这里并不存在零元组和一元组,试着举个例子来说明以下原因:
// u : unit
let u = ()
// x : int
let x = (3)
一对空的小括号表示“unit”类型的唯一值。F#中的“unit”类型与C#语言中的“void”最相似。不需要返回值的函数(因为调用它们只是为了得到它们的副作用)就返回一个unit。而且,不出意料的,把单个值用括号括起来并不会影响它的值和它的类型。(同大部分语言中一样,括号的一个主要目的仅仅是为了在你写表达式的时候需要特殊的执行顺序时重载普通操作符的顺序,像在“3*(2+1)”中。所以“(3)”仅仅是“3”的“括号版本”。)
函数类型
既然我们已经对元组类型和unit类型有了一个基本的理解,让我们来关注一下函数类型。看看下面的这段F#代码:
// f : int -> string -> unit
let f x s =
printfn "%d %s" x s
// g : int * string -> unit
let g(x,s) =
printfn "%d %s" x s
f 3 "foo" // prints 3 "foo"
g(3, "foo") // prints 3 "foo"
函数“f”和“g”相似,但是并不相同。不同之处在于他们是怎样接受参数的。f的参数是柯里化的,然而g的参数时元组。这个区别在调用端是很明显的。调用“柯里化”函数时它的参数是用空白分开的,而调用“tupled”函数时,它的参数是一列用括号括起来并用逗号分开的一些值。我们已经知道一列用小括号括起来用逗号分开的值组成一个元组。所以 你可能猜想你也可以这样写:
g t2 // prints 3 "foo"
你是对的,g的类型“int*string->unit”已经说清楚了。通常,“A->R”已经给出了一个接受A类型参数返回一个R型值得函数的名称。所以g的类型已经告诉我们它接受一个“整形-数组型”二元组返回unit类型。明白了吗?
那么f呢?他的类型是“int->string->unit”。箭头“->”是向右关联的,这就说明这个类型可以被认为是“int->(string->unit)”。所以我暂时定义一个“A”为“int”型,定义一个“R”为“string->unit”型,我们将看到f是一个接受A并返回R的函数——也就是说,f是一个接受“int”型并 返回“接受string型返回unit型的函数”的一个函数。如果我在调用f的时候加了括号就更明显了:
(f 3) "foo" // prints 3 "foo"
函数应用向左关联,这就意味着对f的调用和前面的是一回事。因此我们可以把这段代码解释为:调用带一个参数的函数f,f接受一个int并返回一个新的带一个参数的函数,这个函数接受一个string返回一个unit。为了说的更清楚:
// fp : string -> unit
let fp = f 4
fp "bar" // prints 4 "bar"
这里我只给了f一个参数并且把返回值命名为“fp”。它具有我们所期待的类型——一个接受string型并返回unit型的函数。调用函数时所给的参数比它实际所期待的参数少(生成一个新的函数来接受剩余的参数)的这种技术叫做“partial application”。我们把“fp”叫做“f”的部分应用。它已经接受了一个int了,但是它还在等着接受一个string。这就是“柯里化”形式函数的本质。省略掉后面的参数就很容易的实现了部分应用。
部分应用也可以用在接受元组参数的函数上,不过这样做需要一个显示的lambda。看下面:
// gp : string -> unit
let gp = fun s -> g(4, s)
gp "bar" // prints 4 "bar"
这里我们对g做了同样的操作,我们给了他一个int(值为4)并把结果命名为“gp”。gp还在等待string,这就是说我们我们调用它的时候只需要像例子中那个只给一个string。注意:为了部分应用g,我们必须使用lambda。这就是他在大部分编程语言(那些没有把柯里化语法内置的语言。)中的应用;看一个例子,Dustin Campbell的partial application in C#。
尽管有关F#类型系统与函数类型(特别是当调用.net中带有多个参数的方法)具体怎么工作还有许多需要详细描述,我们今天还是覆盖了很多新的领域。今天我想说的重点是:像下面这样思考你就可以得到一个很好的思维模型。
· F#函数类型被写做“A->R”
· 所有的F#函数接受一个参数。但是:
o 参数类型可以是一个元组,意思是你可以有这样的调用“g(x,s)”,并且
o 返回值类型可以是另外一个函数,意思是你可以有这样的调用“f x s”。
通常,在写F#代码时,柯里化方式是优先考虑的。因为柯里化函数在调用端可以避免很多不必要的括号,而且,柯里化函数允许通过省略后面的参数方便的部分应用。你可能已经在一些我以前写的博客中看多关于后面这一点的例子,应为部分应用在管线操作中很常见。
例子
我们来看一个小例子加深一下印象。假设我有一个列表,我想得到一个只含有列表中奇数的新列表。我能想象一下写一个叫做“JustTheOdds”的函数:
let origList = [1; 2; 3; 4; 5]
let newList = JustTheOdds origList
printfn "%A" newList // prints [1; 3; 5]
If you are aware of the "filter" function from the List module, then writing "JustTheOdds" is easy:
let IsOdd x =
(x % 2) = 1
// justTheOdds : list<int> -> list<int>
let JustTheOdds l =
List.filter IsOdd l
然而在实际应用中我并不喜欢写一个这样的函数。“JustTheOdds”并没有增加多少功能,特别是和“List.filter IsOdd”比起来。都是接受一个list<int>并返回一个list<int>的函数。我们来近距离的看一看表达式“List.filter IsOdd”所描述的值得类型。
// List.filter : ('a -> bool) -> list<'a> -> list<'a>
// IsOdd : int -> bool
// (List.filter IsOdd) : list<int> -> list<int>
list.filter是一个柯里化的函数,它的第一个参数是一个接受’a并返回bool型的一个函数。(旁白:在F#中,泛型参数被命名为一个标识符前面加一个撇号,在C#中我们通常用“T”来代替。在任何情况下,list<’a>和C#中的list<T>一样——filter是一个接受一个一个类型参数的泛型函数。)它的结果是一个新的函数,这个函数接受一个list并且返回一个list。 IsOdd符合list.filter的第一个参数(泛型参数’a被绑定到“int”型上)。结果,当我们应用list.filter到IsOdd上,我们得到一个接受list<int>参数并返回一个list<int>的结果的函数。这正是我们所需要的。所以,我更喜欢把代码写成这样:
let origList = [1; 2; 3; 4; 5]
let newList = origList |> List.filter IsOdd
printfn "%A" newList // prints [1; 3; 5]
这里我们把原始的列表传送给一个列表转换函数——这个函数是部分应用list.filter到IsOdd上的结果。因为这个例子很简单,我可以只写成这样:
let newList = List.filter IsOdd origList
这就是说,这个例子并不需要部分实现和管道传输。但是如果我们对列表作一系列复杂的转换时,管道传输模式和部分实现就有了用武之地。事实上,我们几经在先前关于管道传输的博客中看到这些,在那里我们可以看到在Set模块中有许多函数的部分应用。
总的来说,柯里化函数的部分应用是一个很方便的技术,在实际应用中我发现这个技术最常用在管道传输中。
(Teaser:我已经有写一个使用部分实现的更详细例子的主意,例子中使用部分实现的方法更有趣,我希望能在不久的将来把它写出来。)
打印 | 张贴于 2009-10-21 10:39:40 | Tag:暂无标签
留言反馈