我不明白这个映射元组键编译错误,在 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.IDictionary
,TryGetValue
方法就是在该接口上定义的。每个类似字典的类型,包括 Map
,通常都应该实现该接口。所以给你。
这是一个函数:
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 typeSide
这种误解将方法调用的 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.IDictionary
,TryGetValue
方法就是在该接口上定义的。每个类似字典的类型,包括 Map
,通常都应该实现该接口。所以给你。