首页 > 解决方案 > 我不明白这个映射元组键编译错误,在 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<Instrument * Side, PositionData> 类型

如果我将第二行修改为:

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

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

[FS0001] 此表达式应具有“Instrument * Side”类型,
但此处具有“Instrument”类型

但:

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

将编译...

这是为什么?

标签: f#tuples

解决方案


这是由于方法调用语法。

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

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

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

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

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

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

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

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

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

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

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

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

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

这是可行的,因为现在它不再是“方法调用”语法,因为括号不会立即跟随方法名称。


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

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

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

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

兼容性。与尴尬和荒谬的事情一样,答案是:兼容性。

标准的 .NET 库具有此接口System.Collections.Generic.IDictionary,并且在该接口上TryGetValue定义了方法。并且每个类字典类型,包括Map,通常都期望实现该接口。所以给你。


推荐阅读