【问题标题】:F# mysterious differences between piping and function applicationF# 管道和函数应用程序之间的神秘差异
【发布时间】:2025-11-27 19:50:03
【问题描述】:

前提:我认为管道运算符只不过是语法糖,因此x |> f应该与f(x)完全相同。

类似地,我认为f (fun x -> foo)等价于let g = fun x -> foo; f g

但显然有一些我不明白的差异。

示例 1:

static member contents = 
    let files = Directory.EnumerateFiles (__SOURCE_DIRECTORY__+ @"\foo\bar.txt")
    let fileList = List.ofSeq files
    fileList |> List.map (fun f -> TestCaseData(f).SetName(""))

这很好用:TestCaseData 期望 arg:objf 匹配,而 string 又被推断为 string,因为 fileList 是文件名列表。 但是以下不起作用

static member contents = 
    let files = Directory.EnumerateFiles (__SOURCE_DIRECTORY__+ @"\foo\bar.txt")
    let fileList = List.ofSeq files
    List.map (fun f -> TestCaseData(f).SetName("")) fileList

除了最后一行没有改变。突然f 被推断为obj []TestCaseData 需要obj [] 类型的参数,因此我得到一个错误

Error   1   Type mismatch. Expecting a obj [] list but given a string list    
The type 'obj []' does not match the type 'string'

我原以为两个 sn-ps 在生成正确代码方面是等效的,但只有第一个可以?!

示例 2:

[<TestCase("nonsense", TestName="Nonsense")>]
member x.InvalidInputs str =
    let lexbuf = Microsoft.FSharp.Text.Lexing.LexBuffer<char>.FromString(str)
    Assert.Throws<FatalError> (fun () -> ParsePkg.parse "Dummy path" lexbuf |> ignore)
    |> ignore

最重要的是一切正常。

[<TestCase("nonsense", TestName="Nonsense")>]
member x.InvalidInputs str =
    let lexbuf = Microsoft.FSharp.Text.Lexing.LexBuffer<char>.FromString(str)
    let ff = fun () -> ParsePkg.parse "Dummy path" lexbuf |> ignore
    Assert.Throws<FatalError> (ff)
    |> ignore

如您所见,我所做的只是通过首先定义let ff = ...(例如出于可读性原因)来提取断言的参数,然后编译器突然指向(ff) 参数并抱怨:

Error   2   This expression was expected to have type TestDelegate but here has type unit -> unit

TestDelegate 是我在这里使用的一种 NUnit,它恰好与 unit-&gt;unit 重合,所以我认为无论如何它都会统一,但这并不重要。为什么类型有可能发生变化,因为我再次相信已经做了纯粹的句法替换?!

【问题讨论】:

    标签: f# type-inference piping


    【解决方案1】:

    类型推断是从上到下顺序完成的。所以在第一种情况下,fileList 是第一个 lexical 参数。 然后在管道表达式中使用 fileList 是字符串列表的信息。要知道 string 是否是 f 的合法类型,请使用 TestCaseData 的签名。正如所评论的,基于错误消息 TestCaseData 可能接受 [&lt;Params&gt;] obj [] 使单个字符串参数有效。

    在第二个版本中,除了 TestCaseData 的签名之外,没有其他信息可用于确定 f 的类型,因此推断 f 的类型为 obj []

    在另一个例子中也是如此。恰恰相反。拉出函数会删除它应该是TestDelegate 类型的信息。

    在词汇点上,唯一可用的信息是它是unit-&gt;unit 类型的函数。

    当函数用于需要TestDelegate 的程序点时。类型推断测试函数是否可以用作TestDelegate,如果可以,则推断类型为TestDelegate

    【讨论】:

    • 我还要注意,从错误消息来看,TestCaseData 函数可能不需要obj,而是[&lt;Params&gt;] obj [],当您尝试将其传递给单个obj 时,它会起作用,但在参数类型未知时被推断为obj []
    • 这很有趣。因此,程序的有效性不仅取决于语法和(原则上)类型的有效使用,而且实际上取决于当前实现的类型推断机制。因此,根据微软未来在编译器中类型推断的实现,给定的程序文本是否会变得有效/无效?我发现像(|&gt;) f x = f x 这样的代数定律是有效还是无效取决于fx 的类型,这非常令人不安。
    • 现在我想知道。这个问题是否也会出现在纯 F# 代码中,还是只是因为与 .NET 交互而导致的问题(因为 NUnit 不是用 F# 编写的)?
    • @FriedrichGretz 类型推断不是实现细节。在这些特殊情况下,您可以专门声明类型,并且程序会编译得很好。推断是基于可用的信息。类型是否属于 FSharp.Core 或 mscorlib 或任何其他库的一部分并不相关