为什么 F# 需要 ToDictionary 的类型占位符?

Why does F# require type placeholders for ToDictionary?

给定

[
    1,"test2"
    3,"test"
]
|> dict
// turn it into keyvaluepair sequence
|> Seq.map id

|> fun x -> x.ToDictionary<_,_,_>((fun x -> x.Key), fun x -> x.Value)

如果我没有在 ToDictionary 之后明确使用 <_,_,_>,编译将失败。
Intellisense 工作正常,但编译失败并出现错误:Lookup on object of indeterminate type based on information based on information prior to this program point 所以,看起来,Intellisense 知道如何解析方法调用。

这好像是个线索

|> fun x -> x.ToDictionary<_,_>((fun x -> x.Key), fun x -> x.Value)

失败

Type constraint mismatch.  
The type 'b -> 'c  is not compatible with type IEqualityComparer<'a>     
The type 'b -> 'c' is not compatible with the type 'IEqualityComparer<'a>'  
(using external F# compiler)

x.ToDictionary((fun x -> x.Key), id)

按预期工作

let vMap (item:KeyValuePair<_,_>) = item.Value
x.ToDictionary((fun x -> x.Key), vMap)

我已经在 FSI 和 LinqPad 中复制了该行为。

作为 Eric Lippert 的 reader 的狂热粉丝,我真的很想知道 什么重载决议(或可能来自不同地方的扩展方法)在这里发生冲突,编译器被混淆了?

简短回答:

extension method ToDictionary 定义如下:

static member ToDictionary<'TSource,_,_>(source,_,_)

但是这样称呼:

source.ToDictionary<'TSource,_,_>(_,_)

长答案:

这是您从 msdn 调用的函数的 F# 类型签名。

static member ToDictionary<'TSource, 'TKey, 'TElement> : 
    source:IEnumerable<'TSource> *
    keySelector:Func<'TSource, 'TKey> *
    elementSelector:Func<'TSource, 'TElement> -> Dictionary<'TKey, 'TElement>

但我只指定了两个常规参数:keySelector和elementSelector。这怎么会有源参数?!

源参数其实并没有放在括号里,而是说x.ToDictionary传入的,其中x是源参数。这实际上是一个type extension的例子。这些类型的方法在 F# 等函数式编程语言中非常自然,但在 C# 等面向对象的语言中则更为常见,因此如果您来自 C# 世界,将会感到非常困惑。不管怎样,如果我们看一下 C# 头文件,就会更容易理解发生了什么:

public static Dictionary<TKey, TElement> ToDictionary<TSource, TKey, TElement>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    Func<TSource, TElement> elementSelector
)

因此该方法在第一个参数上使用 "this" 前缀定义,即使它在技术上是静态的。它基本上允许您向已定义的 classes 添加方法,而无需重新编译或扩展它们。这称为原型设计。如果您是 C# 程序员,这种情况很少见,但是像 python 和 javascript 这样的语言会迫使您意识到这一点。以 https://docs.python.org/3/tutorial/classes.html:

为例
class Dog:

tricks = []             # mistaken use of a class variable

def __init__(self, name):
    self.name = name

def add_trick(self, trick):
    self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

方法add_trick以self作为第一个参数定义,但函数被调用为d.add_trick('roll over')。 F# 实际上也很自然地执行此操作,但是以模仿函数调用方式的方式进行。当您声明时:

member x.doSomething() = ...

member this.doSomething() = ...

在这里,您要将函数 doSomething 添加到 "x"/"this" 的原型(或 class 定义)中。因此,在您的示例中,您实际上具有三个类型参数和三个常规参数,但其中一个未在调用中使用。您所要做的就是声明您所做的键选择器函数和元素选择器函数。这就是为什么它看起来很奇怪。

即使预先知道类型,编译器也会混淆采用元素选择器和比较器的重载。 lambda 编译为 FSharpFunc 而不是 C# 中的标准委托类型,如 ActionFunc,并且从一个到另一个的转换确实会出现问题。要使其正常工作,您可以:

为有问题的 Func 提供类型注释

fun x -> x.ToDictionary((fun pair -> pair.Key), (fun (pair : KeyValuePair<_, _>) -> pair.Value)) //compiles

或将参数命名为提示

fun x -> x.ToDictionary((fun pair -> pair.Key), elementSelector = (fun (pair) -> pair.Value))

或强制它选择 3 参数版本:

x.ToLookup((fun pair -> pair.Key), (fun (pair) -> pair.Value), EqualityComparer.Default)

一边

在你的例子中,

let vMap (item:KeyValuePair<_,_>) = item.Value
x.ToDictionary((fun x -> x.Key), vMap)

你会明确地需要注释 vMap 因为编译器无法在没有另一遍的情况下找出 属性 存在的类型。例如,

List.map (fun x -> x.Length) ["one"; "two"] // this fails to compile

这就是管道运算符如此有用的原因之一,因为它可以让您避免类型注释:

["one"; "two"] |> List.map (fun x -> x.Length) // works

List.map (fun (x:string) -> x.Length) ["one"; "two"] //also works