【发布时间】:2014-09-07 22:25:51
【问题描述】:
在 F# 中,let f = fun a -> a-1 和 let f a = a-1 之间有什么区别吗?据我所知,后者只是前者的语法糖。这是正确的吗?
我在这里专门寻找语义差异,而不是任何特定编译器处理这两种情况的差异。
【问题讨论】:
在 F# 中,let f = fun a -> a-1 和 let f a = a-1 之间有什么区别吗?据我所知,后者只是前者的语法糖。这是正确的吗?
我在这里专门寻找语义差异,而不是任何特定编译器处理这两种情况的差异。
【问题讨论】:
(fun a -> ...) 只是一个匿名函数。 F# 与 C# 的不同之处在于函数是一等公民,因此当您将匿名函数绑定到 f 时,它将给 f 类型签名 val f : int -> int(因为 a 被推断为 int32),就像您在第二个示例中绑定了一个普通的命名函数一样。您可以通过在 F# 交互式中运行您的示例来证明它们在语义上是相同的。
编辑:匿名函数甚至支持通常推断但可以明确表示的泛型,如下所示:
let f<'T> = (fun (x: 'T) -> x)
编辑 2:来自 F# 3.1 规范草案(found here):
如果值定义的右侧是匿名函数,则将其视为函数定义,如本例所示:
let f = (fun w -> x + w)
撇开明显的语法错误不谈,这就是说将函数值(即匿名函数)绑定到标识符实际上等同于普通函数定义。它继续说这种等价是“函数式编程的主要内容”,所以你可以很确定这在未来不会改变。
出于一般目的,F# 在设计时将函数定义和函数值(即委托实例、匿名函数)视为平等,但当它需要将 函数定义 提升为 函数时value,它将使用委托类型 FSharpFunc 作为编译类型。这适用于所有高阶函数,例如 Array、List、Seq 模块等中的函数,因为没有像使用委托那样将 CIL 方法用作函数值的实际方法。其他一切看起来都与您期望编译的 C# 或 VB.NET 完全一样——它只是 F# 使用 FSharpFunc 的委托。
编辑 3:我还不能添加 cmets,但是关于 Tomas 的回答,F# 编译器不知道如何概括表达式 let h = (); fun a -> a 但如果您添加类型注释它会接受它你自己,就像尼康的 id 例子一样。
编辑 4:这是 F# 如何编译函数的非常粗略的图片。请注意 Tomas 的示例(他称之为排序表达式)如何变成 FSharpFunc,而没有 (); 的等效函数变成了实际的 CIL 方法。这就是上面提到的 F# 规范。此外,当您使用常规 CIL 方法作为值时,使用部分应用程序或其他方式,编译器将生成 FSharpFunc 以将其表示为闭包。
【讨论】:
正如其他人已经提到的,简单的答案是使用let foo x = ... 和使用let foo = ... 定义一个函数可以给你不同的结果(因为值限制),但在这种情况下不会发生,因为编译器是足够聪明,知道它可以安全地将let foo = fun x -> .. 视为let foo x = ...。
现在,如果您想查看更多详细信息,请尝试以下定义:
let f a = a // Function 'a -> 'a
let g = fun a -> a // Function 'a -> 'a
let h = (); fun a -> a // error FS0030: Value restriction
因此,编译器将使用let foo = fun x -> ... 定义的函数视为普通函数,只要在创建和返回函数的代码之前没有其他内容即可。如果你在此之前做了任何事情(即使它只是忽略了单位值),那么就会有所不同。
如果您想了解更多信息,则 F# 3.0 规范中的 14.6.7 泛化 部分列出了所有 泛化 值(即上述表达式作品):
以下表达式是可推广的:
- 函数表达式
- 实现接口的对象表达式
- 委托表达式
- “let”定义表达式,其中定义的右侧和表达式的主体都是可泛化的
- “let rec”定义表达式,其中所有定义的右侧和表达式主体都是可泛化的
- 元组表达式,其所有元素都是可泛化的
- 记录表达式,其所有元素都是可概括的,其中记录不包含可变字段
- 联合 case 表达式,其所有参数都是可泛化的
- 一个异常表达式,其所有参数都是可泛化的
- 空数组表达式
- 一个常量表达式
- 具有 GeneralizableValue 属性的类型函数的应用程序。
重要的是第一点——函数表达式(例如fun x -> x)是可泛化的;排序表达式(例如(); fun x -> x)是不可泛化的,因此它的行为与普通函数不同。
【讨论】:
您提供的示例在语义上是相同的,但 F# 编译器与它有关。
让我们看一个不同的(通用)函数:
// val singleton1 : x:'a -> List<'a>
let singleton1 x = [x]
// val singleton2 : x:'a -> List<'a>
let singleton2 = fun x -> [x]
如您所见,签名是相同的。但是如果你仔细想想,这两个应该不一样:第一个是一个真正的函数(编译为 .NET 方法),但第二个只是一个保存函数的值(C# 中的委托或 Func )。但是 .NET 运行时中没有通用值。这只是因为 F# 编译器足够聪明,可以将 singleton2 也变成一个函数。
你可以在这里看到我的意思:
let singleton3 = id >> fun x -> [x]
现在我们比 F# 编译器更聪明,但由于 Value Restriction(向下滚动到主题),它无法编译,即使它在语义上应该与 singleton2 相同。
因此,总结一下:从语义的角度来看,您的定义是相同的,但是由于 .NET 运行时的限制,F# 编译器必须做一些额外的工作才能启用它。
我记得的另一个区别是只有函数可以标记inline:
let inline f a = a-1 // OK
let inline f = fun a -> a-1 // Error
【讨论】:
singleton3,提供必要的类型可以解决问题:let singleton3<'a> = id >> fun (x:'a) -> [x] 有效,正如@king jah 在另一个答案中解释的那样。至于“真正的函数”和“持有函数的值”的区别,我的理解是 F# 两者都使用FSharpFunc<>。你知道有不同说法的消息来源吗?
FSharpFunc<>,而且我不知道有任何间接的来源会说……但如果存在的话,托马斯会;)
let inline f<'a> = fun a -> a-1 有效! (let inline h<'a> = 7 也是如此)。我不知道为什么。接受这些在语义上没有多大意义。 @TomasPetricek 引用的 14.6.7 似乎并不适用。也许如果你把这个例子作为一个单独的 StackOverflow 问题提出来,有人可以帮助我们双方理解它。据我所知,这只是规范中的一个漏洞或编译器的问题。
inline”。所以至少let inline h<'a> = 7,它的类型是int,不应该被允许。而我不明白为什么不允许let inline f = fun a -> a-1。嗯!