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

I don't understand this map tuple key compilation error, in F#

这是一个函数:

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
    )

它按预期编译。

让我们关注两行:

let key = (x.Instrument, x.Side)
match brain.Positions.TryGetValue key with

brain.Positions 是一个 Map 类型

如果我修改第二行为:

match brain.Positions.TryGetValue (x.Instrument, x.Side) with

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

[FS0001] This expression was expected to have type 'Instrument * Side'
but here has type 'Instrument'

但是:

match brain.Positions.TryGetValue ((x.Instrument, x.Side)) with

将编译...

这是为什么?

将来,请考虑 How to create a Minimal, Reproducible Example 下提供的 Stack Overflow 指南。好吧,您问题中的代码是最小且可重现的,但它也应该是完整的...

…Complete – Provide all parts someone else needs to reproduce your problem in the question itself

也就是说,当给出以下定义时,您的代码将编译:

type Instrument() = class end
type Side() = class end
type PositionData = { Instrument : Instrument; Side : Side; }
    with member __.UpdateTime = 0
module brain =
    let Positions = dict[(Instrument(), Side()), {Instrument = Instrument(); Side = Side()}]
let positions = []

现在,这是为什么呢?从技术上讲,这是因为 §14.4 方法应用程序解决方案中 F# 4.1 Language Specification 中描述的机制,4.c.,第二个要点:

If all formal parameters in the suffix are “out” arguments with byref type, remove the suffix from UnnamedFormalArgs and call it ImplicitlyReturnedFormalArgs.

相关方法调用的签名支持这一点: System.Collections.Generic.IDictionary.TryGetValue(key: Instrument * Side, value: byref<PositionData>)

此处,如果未提供第二个参数,编译器将隐式转换为元组 return 类型,如 §14.4 5.g.

中所述

您显然熟悉这种行为,但可能不熟悉以下事实:如果您指定两个参数,编译器会将其中的第二个视为显式 byref“out”参数,并相应地报错下一个错误留言:

Error 2 This expression was expected to have type PositionData ref but here has type Side

这种误解将方法调用的 return 类型从 bool * PositionData 更改为 bool,从而引发第三个错误:

Error 3 This expression was expected to have type bool but here has type 'a * 'b

简而言之,你自己发现的带有双括号的解决方法确实是告诉编译器的方法:不,我只给你一个参数(一个元组),这样你就可以隐式地将 byref 转换为“out”元组 return 类型的参数。

这是方法调用语法的问题。

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

这个方法,你看,实际上有两个参数,而不是一个。如您所料,第一个参数是一个键。第二个参数在 C# 中称为 out 参数 - 即第二个 return 值。它原本打算在 C# 中调用的方式是这样的:

Dictionary<int, string> map = ...
string val;
if (map.TryGetValue(42, out val)) { ... }

TryGetValue 的“常规”return 值是一个布尔值,表示是否找到了密钥。而“额外”return 值,此处表示为 out val,是对应于键的值。

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

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

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

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

brain.Positions.TryGetValue (x.Instrument, x.Side)
                             ^             ^
                             first arg     |
                                           second arg

在这种情况下,它希望第一个参数的类型为 Instrument * Side,但您显然只传递了一个 Instrument。这正是错误消息告诉您的内容:“expected to have type 'Instrument * Side' 但这里有类型 'Instrument'".

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

顺便说一句,以下内容也有效:

brain.Positions.TryGetValue <| (x.Instrument, x.Side)

之所以可行,是因为现在它不再是“方法调用”语法,因为括号不会紧跟在方法名称之后。


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

在此特定示例中,使用 Map.tryFind 而不是 .TryGetValue。这是同一件事,但采用适当的函数形式。不是方法。一个函数。

brain.Positions |> Map.tryFind (x.Instrument, x.Side)

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

兼容性。对于尴尬和荒谬的事情,答案总是:兼容性。

标准 .NET 库具有此接口 System.Collections.Generic.IDictionaryTryGetValue 方法就是在该接口上定义的。每个类似字典的类型,包括 Map,通常都应该实现该接口。所以给你。