【问题标题】:Difference between `let f = fun a -> a-1` and `let f a = a-1` in F#F#中`let f = fun a -> a-1`和`let f a = a-1`的区别
【发布时间】:2014-09-07 22:25:51
【问题描述】:

在 F# 中,let f = fun a -> a-1let f a = a-1 之间有什么区别吗?据我所知,后者只是前者的语法糖。这是正确的吗?

我在这里专门寻找语义差异,而不是任何特定编译器处理这两种情况的差异。

【问题讨论】:

    标签: f# semantics


    【解决方案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 以将其表示为闭包。

    【讨论】:

    • 谢谢,非常全面!正是我正在寻找的带有保证的文本。我不了解围绕此操作的社区文化,但我相信您的回答现在比@TomasPetricek 的回答更好,所以我接受了这个。
    【解决方案2】:

    正如其他人已经提到的,简单的答案是使用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)是不可泛化的,因此它的行为与普通函数不同。

    【讨论】:

      【解决方案3】:

      您提供的示例在语义上是相同的,但 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&lt;'a&gt; = id &gt;&gt; fun (x:'a) -&gt; [x] 有效,正如@king jah 在另一个答案中解释的那样。至于“真正的函数”和“持有函数的值”的区别,我的理解是 F# 两者都使用FSharpFunc&lt;&gt;。你知道有不同说法的消息来源吗?
      • 你是对的。 F# 对两者都使用FSharpFunc&lt;&gt;,而且我不知道有任何间接的来源会说……但如果存在的话,托马斯会;)
      • 奇怪的是,let inline f&lt;'a&gt; = fun a -&gt; a-1 有效! (let inline h&lt;'a&gt; = 7 也是如此)。我不知道为什么。接受这些在语义上没有多大意义。 @TomasPetricek 引用的 14.6.7 似乎并不适用。也许如果你把这个例子作为一个单独的 StackOverflow 问题提出来,有人可以帮助我们双方理解它。据我所知,这只是规范中的一个漏洞或编译器的问题。
      • 更新:我检查了,这似乎是编译器错误?根据3.1 draft spec 中的14.6.3,“值定义可能不是inline”。所以至少let inline h&lt;'a&gt; = 7,它的类型是int,不应该被允许。而我不明白为什么不允许let inline f = fun a -&gt; a-1。嗯!
      猜你喜欢
      • 1970-01-01
      • 2021-07-06
      • 2016-09-27
      • 2019-10-22
      • 1970-01-01
      • 1970-01-01
      • 2021-12-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多