当我使用: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
?从程序员的角度来看它是否重要?
答案 0 :(得分:8)
类型变量的名称并不重要。类型:
Eq element => element -> [element] -> Bool
完全相同:
Eq a => a -> [a] -> Bool
有些名字更容易阅读/记忆。
现在,推理器如何为类型选择最佳名称?
免责声明:我绝对不是 GHC开发人员。但是我在我的学士论文中为Haskell开发了一个类型推理器。
在推理期间,为变量选择的名称可能不具有可读性。事实上,他们几乎肯定是_N
N
一个数字或aN
N
数字。
这是因为你经常需要刷新"键入变量以完成推理,因此您需要一种快速创建新名称的方法。为此,使用编号变量非常简单。
推理完成后显示的名称可以打印出来#34;。推理器可以重命名变量以使用a
,b
,c
等,而不是_1
,_2
等。
诀窍是大多数操作都有明确的类型签名。某些定义需要量化某些类型变量(例如class
,data
和instance
。
用户明确提供的所有这些名称都可用于以更好的方式显示类型。
在推理时,您可以以某种方式跟踪新鲜类型变量的来源,以便能够在向用户显示时更加明智地重命名它们。
另一种选择是通过向它们添加数字来刷新变量。例如,一种新的return
类型可能是Monad m0 => a0 -> m0 a0
(我们知道使用m
和a
只是因为class
Monad
的定义使用这些名称)。推理完成后,你可以摆脱数字并获得漂亮的名字。
通常,推理器将尝试使用通过签名明确提供的名称。如果已使用此类名称,则可能决定添加一个数字而不是使用其他名称(例如,如果b1
已绑定,请使用c
代替b
。
可能还有其他一些临时规则。例如,元组元素具有t
,t1
,t2
,t3
等的事实可能是使用自定义规则完成的。事实上,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 equivalence和alpha 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等价,您在示例中选择a
或t
并不重要。选择类型变量名称以最大化人类的便利是一个完全不同的主题,可能要复杂得多!