如果编译器可以在编译时“告诉”您正在使用的实际类型,那么方法查找会在编译时发生。否则它会在运行时发生。如果查找发生在编译时,方法代码可能会被内联,具体取决于方法的大小。(这也适用于常规函数:如果编译器知道您正在调用哪个函数,如果该函数“足够小”,它将内联它。)
例如,考虑(sum [1 .. 10]) :: Integer
。在这里,编译器静态知道该列表是一个 take 列表Integer
,因此它可以内联+
函数 for Integer
。另一方面,如果你做类似的事情
foo :: Num x => [x] -> x
foo xs = sum xs - head x
然后,当您调用 时sum
,编译器不知道您使用的是什么类型。(这取决于赋予的类型foo
),因此它不能进行任何编译时查找。
另一方面,使用{-# SPECIALIZE #-}
pragma,您可以执行类似的操作
{-# SPECIALIZE foo:: [Int] -> Int #-}
这样做是告诉编译器编译foo
输入是Int
值列表的特殊版本。这显然意味着对于该版本,编译器可以在编译时进行所有方法查找(并且几乎可以肯定将它们全部内联)。现在有两个版本foo
- 一个适用于任何类型并进行运行时类型查找,另一个仅适用于Int
,但 [可能] 快得多。
当你调用foo
函数时,编译器必须决定调用哪个版本。如果编译器可以在编译时“告诉”您想要该Int
版本,它就会这样做。如果它不能“告诉”你将使用什么类型,它将使用较慢的任何类型版本。
请注意,您可以对单个函数进行多个专业化。例如,你可以做
{-# SPECIALIZE foo :: [Int] -> Int #-}
{-# SPECIALIZE foo :: [Double] -> Double #-}
{-# SPECIALIZE foo :: [Complex Double] -> Complex Double #-}
现在,只要编译器知道您正在使用其中一种类型,它就会使用为该类型硬编码的版本。但是如果编译器不能告诉你使用的是什么类型,它就永远不会使用专门的版本,而且总是使用多态版本。(例如,这可能意味着您需要专门化调用 的函数foo
。)
如果您浏览编译器的核心输出,您可能可以准确地弄清楚它在任何特定情况下做了什么。不过,您可能会发疯似的发疯……