考虑在模式中引入的变量,例如此Haskell示例中的f
:
case (\x -> x) of f -> (f True, f 'c')
由于f
的两种不同用法,此代码导致类型错误(“无法将预期的类型”布尔”与实际类型”字符”匹配)。它表明f
的推断类型在Haskell中不是多态的。
但是f
为什么不应该是多态的?
我有两个比较点:OCaml和“教科书” Hindley-Milner。两者都建议f
应该是多态的。
在OCaml中,类似的代码不是错误:
match (fun x -> x) with f -> (f true, f 'c')
这将得出类型为(true, 'c')
的{{1}}。因此,看起来OCaml可以很好地为bool * char
分配一个多态类型。
我们可以通过将事情分解为Hindley-Milner的基本知识(Hastkell和OCaml都基于“让”的lambda微积分)来获得明确性。当简化为这个核心系统时,当然就没有模式匹配之类的东西。但是我们可以得出相似之处。在“ let”和“ lambda”之间,f
比case expr1 of f -> expr2
更接近let f = expr1 in expr2
。 “ case”(如“ let”)在语法上限制(lambda f. expr2) expr1
绑定到f
,而函数expr1
不知道绑定lambda f. expr2
的原因,因为函数在程序中的何处没有这种限制。这就是为什么让约束变量在Hindley-Milner中得到泛化而使与lambda绑定的变量没有得到泛化的原因。看来,允许对约束变量进行泛化的相同推理也表明,模式匹配引入的变量也可以泛化。
为清楚起见,上面的示例极少,因此它们在模式匹配中仅显示了普通模式f
,但是所有相同的逻辑都扩展到了f
之类的任意复杂的模式,可以引入多个变量一切都会被概括。
我的分析正确吗?特别是在Haskell中-认识到它不仅是普通的Hindley-Milner而不是OCaml-为什么在第一个示例中我们不归纳Just (a:b:(x,y):_)
的类型?
这是一个明确的语言设计决定吗?如果是,原因是什么? (我注意到some in the community think that not even "let" should be generalized,但我可以想象设计决策早于该论文。)
如果将模式中引入的变量设置为类似于“ let”的多态性,是否会在很大程度上破坏与Haskell其他方面的兼容性?
答案 0 :(得分:3)
如果我们将多态类型(forall x. t
)分配给case scrutinee,则它不匹配任何非平凡的模式,因此没有必要使用case
。
我们可以用其他有用的方法来概括一下吗?并非如此,因为GHC缺乏对“强制性”实例化的支持。在您的Just (a:b:(x,y):_)
示例中,没有一个绑定变量可以具有多态类型,因为Maybe
,(,)
和[]
不能用此类实例化。
正如注释中所述,一件事情起作用:具有多态字段的数据类型,例如data Endo = Endo (forall a. a -> a)
。但是,从技术上讲,对多态字段进行类型检查不会涉及泛化步骤,也不会像let泛化那样起作用。
原则上,泛化可以在很多点执行,例如,甚至可以在任意函数自变量中执行(例如,在f (\x -> x)
中)。但是,太多的泛化通过引入难以处理的高阶类型阻塞了类型推断。这也可以理解为通过删除未解决的元变量来消除程序不同部分之间有用的类型依赖性。尽管有些系统可以比GHC更好地处理较高等级的推理,最著名的是MLF,但它们也要复杂得多,还没有实际应用。我个人比较喜欢完全没有沉默的概括。
答案 1 :(得分:2)
第一个问题是类型类的泛化并不总是免费的。考虑show :: forall a. Show a => a -> String
和以下表达式:
case show of
f -> ...
如果将f
概括为f :: forall a. Show a => a -> String
,那么GHC将在每次调用Show
时传递一个f
字典,而不是一次出现{{1 }}。如果有多个相同类型的调用,则与不进行泛化相比,这会重复工作。
当与类型类结合使用时,它实际上也不是当前类型推断算法的概括:它可能导致现有程序不再进行类型检查。例如,
show
通过不对 case show of
f -> f () ++ f mempty
进行泛化,我们可以推断出f
的类型为mempty
。另一方面,泛化()
将失去该连接,并且该表达式中f :: forall a. Show a => a -> String
的类型将是不明确的。
尽管这些都是次要问题,但即使没有完全向后兼容,也可以通过某些同构性限制来解决问题。
答案 2 :(得分:0)
除了其他答案,还有一个原因,就是如何在与现有类型的交互方面以模式匹配的方式对待类型变量。让我们来看看Data.Functor.Coyoneda
中的几个定义:
{-# LANGUAGE GADTs #-}
data Coyoneda f a where
Coyoneda :: (b -> a) -> f b -> Coyoneda f a
lowerCoyoneda :: Functor f => Coyoneda f a -> f a
lowerCoyoneda (Coyoneda g x) = fmap g x
Coyoneda
具有一个存在类型变量,该变量用于构造函数的两个参数。如果GHC没有固定该类型,则fmap
中的lowerCoyoneda
将无法进行类型检查。 GHC需要知道g
和x
的类型之间具有适当的关系,这需要在模式匹配中固定类型变量。