我不明白 F# 中的这个映射元组键编译错误

Tho*_*mas 4 f# tuples

这是一个函数:

let newPositions : PositionData list =
    positions
    |> List.filter (fun x ->
        let key = (x.Instrument, x.Side)
        match brain.Positions.TryGetValue key with
        | false, _ ->
             // if we don't know the position, it's new
            true
        | true, p when x.UpdateTime > p.UpdateTime ->
             // it's newer than the version we have, it's new
            true
        | _ ->
            false
    )
Run Code Online (Sandbox Code Playgroud)

它按预期编译。

让我们关注两行:

let key = (x.Instrument, x.Side)
match brain.Positions.TryGetValue key with
Run Code Online (Sandbox Code Playgroud)

Brain.Positions是 Map<Instrument * Side, PositionData> 类型

如果我将第二行修改为:

match brain.Positions.TryGetValue (x.Instrument, x.Side) with
Run Code Online (Sandbox Code Playgroud)

那么代码将无法编译,并出现错误:

[FS0001] 该表达式的类型应为“Instrument * Side”
,但此处的类型为“Instrument”

但:

match brain.Positions.TryGetValue ((x.Instrument, x.Side)) with
Run Code Online (Sandbox Code Playgroud)

将编译...

这是为什么?

Fyo*_*kin 5

这是由于方法调用语法造成的。

TryGetValue不是一个函数,而是一个方法。这是一件非常不同的事情,而且总的来说是一件更糟糕的事情。并遵守一些特殊的语法规则。

你看,这个方法实际上有两个参数,而不是一个。正如您所期望的,第一个参数是键。第二个参数是 C# 中所谓的out参数,即第二个返回值。它最初在 C# 中的调用方式是这样的:

Dictionary<int, string> map = ...
string val;
if (map.TryGetValue(42, out val)) { ... }
Run Code Online (Sandbox Code Playgroud)

的“常规”返回值TryGetValue是一个布尔值,表示是否找到了该密钥。而这里表示的“额外”返回值out val,就是与该键对应的值。

当然,这是极其尴尬的,但它并没有阻止早期的 .NET 库非常广泛地使用这种模式。因此,F# 对这种模式有特殊的语法糖:如果只传递一个参数,那么结果将成为一个由“实际”返回值和参数组成的元组out。这就是您在代码中要匹配的内容。

当然,F# 不能阻止您完全按照设计使用该方法,因此您也可以自由传递两个参数 - 第一个参数是键,第二个参数是单元格byref(在 F# 中相当于out)。

这就是与方法调用语法发生冲突的地方。您会看到,在 .NET 中,所有方法都是非柯里化的,这意味着它们的参数都是有效的元组。因此,当您调用方法时,您正在传递一个元组。

这就是本例中发生的情况:只要添加括号,编译器就会将其解释为尝试使用元组参数调用 .NET 方法:

brain.Positions.TryGetValue (x.Instrument, x.Side)
                             ^             ^
                             first arg     |
                                           second arg
Run Code Online (Sandbox Code Playgroud)

在这种情况下,它期望第一个参数的类型为Instrument * Side,但您显然只传递了一个Instrument. 这正是错误消息告诉您的内容:“预期类型为‘Instrument * Side’,但此处类型为‘Instrument’ ”。

但是,当您添加第二对括号时,含义会发生变化:现在外部括号被解释为“方法调用语法”,内部括号被解释为“表示元组”。所以现在编译器将整个事情解释为一个参数,并且一切都像以前一样工作。

顺便说一句,以下内容也将起作用:

brain.Positions.TryGetValue <| (x.Instrument, x.Side)
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为现在它不再是“方法调用”语法,因为括号不会立即跟在方法名称后面。


但更好的解决方案是一如既往,不要使用方法,而是使用函数!

在此特定示例中,.TryGetValue使用代替Map.tryFind。这是同样的事情,但是以正确的函数形式。不是方法。一个函数。

brain.Positions |> Map.tryFind (x.Instrument, x.Side)
Run Code Online (Sandbox Code Playgroud)

问:但是为什么会存在这种令人困惑的方法呢?

兼容性。对于尴尬和无意义的事情,答案总是:兼容性。

标准 .NET 库具有此接口,并且在该接口上定义了System.Collections.Generic.IDictionary该方法。TryGetValue每个类似字典的类型(包括Map)通常都应该实现该接口。那么就到这里吧。