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

时间:2017-01-06 15:23:06

标签: f# overload-resolution

给出

[
    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工作正常,但编译失败并出现错误:根据此程序点之前的信息查找不确定类型的对象 所以,似乎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的狂热读者,我真的很想知道 什么重载决议,(或可能来自不同地方的扩展方法)在这里是冲突的,编译器被它迷惑了?

2 个答案:

答案 0 :(得分:2)

即使前面已知类型,编译器也会在带有元素选择器和比较器的重载之间混淆。 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

答案 1 :(得分:1)

答案简短:

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”前缀,即使它在技术上是静态的。它基本上允许您向已定义的类添加方法,而无需重新编译或扩展它们。这称为原型设计。如果你是一名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”的原型(或类定义)中。因此,在您的示例中,您实际上有三个类型参数和三个常规参数,但其中一个未在调用中使用。您剩下的就是声明键选择器功能和元素选择器功能。这就是为什么它看起来很奇怪。