Haskell类型推断

时间:2014-07-02 16:44:34

标签: haskell functional-programming

当我使用:t来检查Haskell中的函数类型时,例如my previous question中的函数,我倾向于得到如下结果:

Eq a => a -> [a] -> Bool
(Ord a, Num a, Ord a1, Num a1) => a -> a1 -> a
(Num t2, Num t1, Num t, Enum t2, Enum t1, Enum t) =>  [(t, t1, t2)]

似乎这不是一个微不足道的问题 - Haskell解释器如何选择文字来象征类型类?什么时候会选择a而不是t?什么时候会选择a1而不是b?从程序员的角度来看它是否重要?

3 个答案:

答案 0 :(得分:8)

类型变量的名称并不重要。类型:

Eq element => element -> [element] -> Bool

完全相同:

Eq a => a -> [a] -> Bool

有些名字更容易阅读/记忆。

现在,推理器如何为类型选择最佳名称?

免责声明:我绝对不是 GHC开发人员。但是我在我的学士论文中为Haskell开发了一个类型推理器。

在推理期间,为变量选择的名称可能不具有可读性。事实上,他们几乎肯定是_N N一个数字或aN N数字。

这是因为你经常需要刷新"键入变量以完成推理,因此您需要一种快速创建新名称的方法。为此,使用编号变量非常简单。

推理完成后显示的名称可以打印出来#34;。推理器可以重命名变量以使用abc等,而不是_1_2等。

诀窍是大多数操作都有明确的类型签名。某些定义需要量化某些类型变量(例如classdatainstance。 用户明确提供的所有这些名称都可用于以更好的方式显示类型。

在推理时,您可以以某种方式跟踪新鲜类型变量的来源,以便能够在向用户显示时更加明智地重命名它们。 另一种选择是通过向它们添加数字来刷新变量。例如,一种新的return类型可能是Monad m0 => a0 -> m0 a0(我们知道使用ma只是因为class Monad的定义使用这些名称)。推理完成后,你可以摆脱数字并获得漂亮的名字。

通常,推理器将尝试使用通过签名明确提供的名称。如果已使用此类名称,则可能决定添加一个数字而不是使用其他名称(例如,如果b1已绑定,请使用c代替b

可能还有其他一些临时规则。例如,元组元素具有tt1t2t3等的事实可能是使用自定义规则完成的。事实上,t并未出现在(,,)的签名中。

答案 1 :(得分:3)

How does GHCi pick names for type variables?解释了这些变量名称的数量。正如Ganesh Sittampalam在评论中指出的那样,算术序列似乎发生了一些奇怪的事情。 Haskell 98报告和Haskell 2010报告均表明

[e1..] = enumFrom e1
然而,GHCi给出了以下内容:

Prelude> :t [undefined..]
[undefined..] :: Enum t => [t]

Prelude> :t enumFrom undefined
enumFrom undefined :: Enum a => [a]

这清楚地表明,奇怪的行为与Enum类本身无关,而是在将句法序列翻译为enumFrom形式的某个阶段。我想知道GHC是不是真的使用该翻译,但它确实是:

{-# LANGUAGE NoMonomorphismRestriction #-}
module X (aoeu,htns) where
aoeu = [undefined..]
htns = enumFrom undefined

使用ghc -ddump-simpl enumlit.hs编译

X.htns :: forall a_aiD. GHC.Enum.Enum a_aiD => [a_aiD]
[GblId, Arity=1]
X.htns =
  \ (@ a_aiG) ($dEnum_aiH :: GHC.Enum.Enum a_aiG) ->
    GHC.Enum.enumFrom @ a_aiG $dEnum_aiH (GHC.Err.undefined @ a_aiG)

X.aoeu :: forall t_aiS. GHC.Enum.Enum t_aiS => [t_aiS]
[GblId, Arity=1]
X.aoeu =
  \ (@ t_aiV) ($dEnum_aiW :: GHC.Enum.Enum t_aiV) ->
    GHC.Enum.enumFrom @ t_aiV $dEnum_aiW (GHC.Err.undefined @ t_aiV)

因此这两个表示之间的差异是指定的类型变量名称。我不太了解GHC如何知道t来自哪里,但至少我把它缩小了!


ØrjanJohansen在评论中指出,函数定义和lambda抽象似乎发生了类似的事情。

Prelude> :t \x -> x
\x -> x :: t -> t

Prelude> :t map (\x->x) $ undefined
map (\x->x) $ undefined :: [b]

在后一种情况下,b类型来自map的显式类型签名。

答案 2 :(得分:0)

您熟悉alpha equivalencealpha substitution的概念吗?这样就可以理解,例如,即使它们不同,以下两者都完全等效且可互换(在某些情况下):

\x -> (x, x)
\y -> (y, y)

相同的概念可以扩展到类型和类型变量的级别(有关进一步阅读,请参阅“系统F”)。事实上,Haskell对于绑定类型变量有一个“类型级别的lambdas”的概念,但是很难看到,因为它们默认是隐式的。但是,您可以使用ExplicitForAll扩展名将它们显式化,并使用显式绑定类型变量:

ghci> :set -XExplicitForAll 
ghci> let f x = x; f :: forall a. a -> a

在第二行中,我使用forall关键字引入一个新的类型变量,然后在一个类型中使用。

换句话说,只要类型表达式满足alpha等价,您在示例中选择at并不重要。选择类型变量名称以最大化人类的便利是一个完全不同的主题,可能要复杂得多!