使用这个表达式:
putStrLn $ show $ tryMe (Just 2) 25
我们有以下起始信息可供使用:
putStrLn :: String -> IO ()
show :: Show a => a -> String
tryMe :: Maybe Int -> Int -> Int
Just :: b -> Maybe b
2 :: Num c => c
25 :: Num d => d
(我在这里使用了不同的类型变量,因此我们可以更轻松地在相同的范围内考虑它们)
类型检查器的工作基本上是找到所有这些变量的选择类型,然后确保参数和结果类型匹配,以及所有必需的类型类实例都存在。
在这里,我们可以看到tryMe
应用于两个参数将成为一个Int
,因此a
(作为show
的输入)必须是Int
。这需要存在一个Show Int
实例;确实存在,所以我们完成了对a
的处理。
同样,tryMe
想要一个Maybe Int
,我们有应用Just
的结果。因此,b
必须是Int
,而我们对Just
的使用是Int -> Maybe Int
。
Just
被应用于2 :: Num c => c
。我们决定它必须应用于一个Int
,因此c
必须是Int
。如果我们有Num Int
,那么我们就可以做到这一点,而我们确实有,因此c
已经处理完毕。
这样就只剩下25 :: Num d => d
了。它被用作tryMe
的第二个参数,该函数期望一个Int
,因此d
必须是Int
(再次满足Num
约束)。
然后我们只需要确保所有参数和结果类型都匹配,这是非常明显的。这主要是重申上面的内容,因为我们通过选择类型变量的唯一可能值来使它们匹配,所以我不会详细介绍它。
现在,这有什么不同吗?
putStrLn $ show $ doIt (Just 2) 25
好的,让我们再次看看所有的组成部分:
putStrLn :: String -> IO ()
show :: Show a => a -> String
doIt :: Test t => TT t -> t -> t
Just :: b -> Maybe b
2 :: Num c => c
25 :: Num d => d
show
函数的输入是应用两个参数到
doIt
的结果,因此它是
t
。所以我们知道
a
和
t
是相同类型的,这意味着我们需要
Show t
,但我们还不知道
t
是什么,所以我们需要回过头来看一下。
应用
Just
的结果用于需要
TT t
的地方。所以我们知道
Maybe b
必须是
TT t
,因此
Just :: _b -> TT t
。我使用了GHC的
部分类型签名语法来写
_b
,因为这个
_b
与之前的
b
不同。当我们有
Just :: b -> Maybe b
时,我们可以选择任何我们喜欢的类型来代替
b
,而
Just
可以具有该类型。但现在我们需要一些特定但未知的类型
_b
,使得
TT t
是
Maybe _b
。我们没有足够的信息来知道那个类型是什么,因为我们不知道我们正在使用哪个实例的
TT t
定义,因为我们不知道
t
是什么。
Just
的参数是
2 :: Num c => c
。所以我们可以知道
c
也必须是
_b
,这也意味着我们将需要一个
Num _b
实例。但由于我们还不知道
_b
是什么,所以我们无法检查是否有
Num
实例可用。我们稍后会回来看它。
最后,
25 :: Num d => d
在
doIt
想要一个
t
的地方使用。好的,所以
d
也是
t
,我们需要一个
Num t
实例。同样地,我们仍然不知道
t
是什么,所以我们无法检查这一点。
所以总体而言,我们已经找出了这些:
putStrLn :: String -> IO ()
show :: t -> String
doIt :: TT t -> t -> t
Just :: _b -> TT t
2 :: _b
25 :: t
同时还有以下待解决的限制条件:
Test t, Num t, Num _b, Show t, (Maybe _b) ~ (TT t)
如果您之前没有看到过,~
是我们编写的两个类型表达式必须相同的约束条件。
然后我们就卡住了。我们无法进一步确定,因此GHC将报告类型错误。您引用的特定错误消息抱怨我们无法确定TT t
和Maybe _b
是否相同(它将类型变量称为a0
和a1
),因为我们没有足够的信息选择它们的具体类型(它们是模糊的)。
如果我们为表达式的某些部分添加一些额外的类型签名,我们可以更进一步。添加25 :: Int
1可以立即让我们知道t
是Int
。现在我们可以有所作为!让我们将其修补到我们尚未解决的约束中:
Test Int, Num Int, Num _b, Show Int, (Maybe _b) ~ (TT Int)
Num Int
和
Show Int
是显而易见的内置函数。我们还有
Test Int
,这给了我们定义
TT Int = Maybe Int
。因此,
(Maybe _b) ~ (Maybe Int)
,因此
_b
也是
Int
,这也使我们能够解决
Num _b
限制(它又是
Num Int
)。现在,由于我们已经将所有类型变量填充为具体类型,因此很容易验证所有参数和结果类型是否匹配。
但是为什么你的其他尝试没有成功?让我们回到没有附加类型注释的最远点:
putStrLn :: String -> IO ()
show :: t -> String
doIt :: TT t -> t -> t
Just :: _b -> TT t
2 :: _b
25 :: t
此外还需要解决以下限制:
Test t, Num t, Num _b, Show t, (Maybe _b) ~ (TT t)
然后添加
Just 2 :: Maybe Int
。因为我们知道它也是
Maybe _b
和
TT t
,这告诉我们
_b
是
Int
。我们现在也知道我们正在寻找一个
Test
实例,它给出了
TT t = Maybe Int
。但这并不能确定
t
是什么!有可能还有以下情况:
instance Test Double where
type TT Double = Maybe Int
doIt (Just a) _ = fromIntegral a
doIt Nothing b = b
现在可以将
t
选择为
Int
或
Double
,两者都可以正常工作(因为
25
也可以是
Double
),但会打印不同的内容!
诱人的想法是抱怨因为只有一个实例
t
符合
TT t = Maybe Int
,所以我们应该选择它。但是实例选择逻辑没有这样猜测。如果你面临的情况可能存在另一个匹配实例,但由于代码错误而不存在(例如忘记导入定义它的模块),那么它不会选择唯一可见的匹配实例。只有当它知道没有其他实例可以适用时,才会选择实例。
因此,“只有一个实例符合
TT t = Maybe Int
”的论点不能让GHC倒推出
t
可以是
Int
。
总的来说,对于类型族,你只能“向前工作”;如果你知道你正在应用类型族的类型,你可以从中得出结果类型应该是什么,但是如果你知道结果类型,这并不能确定输入类型。这通常很令人惊讶,因为普通的类型构造函数确实让我们可以这样“向后工作”;我们使用上面的方法从
Maybe _b = Maybe Int
推断出
_b = Int
。这只有在新的
data
声明中适用,应用类型构造函数始终保留结果类型中的参数类型(例如,当我们将
Maybe
应用于
Int
时,结果类型为
Maybe Int
)。相同的逻辑在类型族中不起作用,因为可能存在多个类型族实例映射到相同的类型,即使没有,也没有要求结果类型中的某些内容与输入类型之间存在可识别的模式(我可以使用
type TT Char = Maybe (Int -> Double, Bool)
)。
因此,当您需要添加类型注释时,通常会发现在其类型为类型族结果的位置添加注释无法正常工作,而需要确定类型族的输入(或其他必须与其相同类型的内容)。
1 请注意,在您的问题中引用的代码 main = putStrLn $ show $ doIt (Just 2) 25::Int
实际上是行不通的。 :: Int
签名绑定“尽可能远”,因此您实际上声称整个表达式 putStrLn $ show $ doIt (Just 2) 25
的类型为 Int
,但它必须是 IO ()
类型。我假设您在真正检查时在 25 :: Int
周围加上了括号,所以应该是 putStrLn $ show $ doIt (Just 2) (25 :: Int)
。
2 GHC 有关于什么被认为是“确定的知识”以至于不存在其他匹配实例的特定规则。我不会详细讲解,但基本上当您有 instance Constraints a => SomeClass (T a)
时,它必须能够通过仅考虑 SomeClass (T a)
来明确选择一个实例; 它不能看左边的约束条件 =>
符号。
instance Test Integer
,这样实例的选择就会变得模糊不清。 - amalloyMaybe Int
参数上时,类型注释没有产生相同的影响”)。回答者似乎选择忽略第二个问题(也许是正确的),所以你应该为第二个问题发布一个单独的问题。简短的答案是,类型族不是可逆的。 - user2407038