我什么时候需要打字注释?

时间:2017-07-11 08:22:57

标签: haskell types typeclass type-families

考虑这些功能

{-# LANGUAGE TypeFamilies #-}

tryMe :: Maybe Int -> Int -> Int
tryMe (Just a) b = a
tryMe Nothing b  = b

class Test a where
    type TT a
    doIt :: TT a -> a -> a

instance Test Int where
    type TT Int = Maybe Int
    doIt (Just a) b  = a
    doIt (Nothing) b = b

这有效

main = putStrLn $ show $ tryMe (Just 2) 25

这不是

main = putStrLn $ show $ doIt (Just 2) 25
{- 
  • Couldn't match expected type ‘TT a0’ with actual type ‘Maybe a1’
  The type variables ‘a0’, ‘a1’ are ambiguous
-}

但是,如果我指定第二个参数的类型,它确实有效

main = putStrLn $ show $ doIt (Just 2) 25::Int

两个函数的类型签名似乎相同。为什么我需要为类型类函数注释第二个参数?另外,如果我只注释Maybe Int的第一个参数,它仍然无法正常工作。为什么呢?

3 个答案:

答案 0 :(得分:10)

  

我什么时候需要在Haskell中转换类型?

仅在非常模糊,伪依赖类型的设置中,编译器无法证明两种类型相同,但您知道它们是;在这种情况下,您可以unsafeCoerce他们。 (这就像C ++'reinterpret_cast,即它完全绕过类型系统,只是将内存位置视为包含你告诉它的类型。这确实是非常不安全! )

然而,这不是你在这里所说的。添加像::Int这样的本地签名执行任何强制转换,它只是向类型检查器添加提示。需要这样的提示应该不足为奇:你没有指明a应该是什么; show的输入是多态的,输出中是doIt多态的。但是编译器在解析关联的TT之前必须知道它是什么;选择错误的a可能导致与预期完全不同的行为。

更令人惊讶的是,有时您可以省略此类签名。这是可能的原因是Haskell,更多GHCi,defaulting rules。当你写例如show 3,您再次有一个含糊不清的a类型变量,但GHC认识到Num约束可以由Integer类型“自然地”实现,所以它只需要挑。
在快速评估REPL中的某些内容时,默认规则很方便,但它们依赖于它们,因此我建议您永远不要在正确的程序中执行

现在,这并不意味着您应该始终向任何子表达式添加:: Int个签名。它确实意味着,作为一项规则,您应该致力于使函数参数始终比结果更少多态。我的意思是:如果可能的话,任何本地类型变量都应该可以从环境中删除。然后,指定最终结果的类型就足够了。

不幸的是,show违反了这个条件,因为它的参数是多态的,变量a根本没有出现在结果中。因此,这是您无法获得某些签名的功能之一。

答案 1 :(得分:4)

所有这些讨论都很好,但尚未明确说明在Haskell中数字文字是多态的。你可能知道这一点,但可能没有意识到它与这个问题有关。在表达式

doIt (Just 2) 25

25没有类型Int,它的类型为Num a => a - 也就是说,它的类型只是一些数字类型,等待额外的信息将其精确地固定下来。这使得这个棘手的原因是特定的选择可能会影响第一个参数的类型。因此,amalloy的评论

  

GHC担心某人可能会定义instance Test Integer,在这种情况下,实例的选择将不明确。

当您提供该信息时 - 可以来自参数或结果类型(因为a -> a签名的doIt部分) - 通过编写任何一个

doIt (Just 2) (25 :: Int)
doIt (Just 2) 25 :: Int   -- N.B. this annotates the type of the whole expression

然后知道具体实例。

请注意,您不需要类型系列来产生此行为。这是类型类分辨率课程的标准。出于同样的原因,以下代码将产生相同的错误。

class Foo a where
    foo :: a -> a

main = print $ foo 42

您可能想知道为什么不会发生类似

的事情
main = print 42

这是一个很好的问题,leftroundabout已经解决了。它与Haskell的defaulting rules有关,它们非常专业,我认为它们只不过是黑客攻击。

答案 2 :(得分:2)

使用这个表达式:

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的输入)必须为{{1} }}。这要求有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。我们知道ta属于同一类型,这意味着我们需要t,但我们还不知道Show t是什么,所以我们&#39 ;我必须回到那。

应用t的结果用于我们想要Just的位置。因此,我们知道TT t必须是Maybe b,因此TT t。我使用GHC的partial type signature syntax撰写了Just :: _b -> TT t,因为_b与我们之前的_b不同。当我们b时,我们可以为Just :: b -> Maybe b选择我们喜欢的任何类型,而b可以选择该类型。但现在我们需要一些特定但未知的类型Just,以使_bTT t。我们没有足够的信息来了解该类型的内容,因为我们不知道Maybe _b我们不知道t我们定义了哪个实例的定义使用

TT 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 _ba0),因为我们没有足够的信息来为他们选择具体类型(它们含糊不清)。

如果我们为表达式的某些部分添加一些额外的类型签名,我们可以更进一步。立即添加a1 1 可让我们宣读25 :: Intt。现在我们可以到达某个地方!让补丁进入我们尚未解决的限制:

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,这也告诉我们_bInt。我们现在也知道我们正在寻找一个给我们Test的{​​{1}}个实例。但这并不能确定TT t = Maybe Int是什么!可能还有:

t

现在选择instance Test Double where type TT Double = Maybe Int doIt (Just a) _ = fromIntegral a doIt Nothing b = b 作为tInt是有效的;要么你的代码可以正常工作(因为Double也可以是25),但会打印不同的东西!

很容易抱怨,因为Double只有t的一个实例我们应该选择那个TT t = Maybe Int。但是实例选择逻辑被定义为不以这种方式猜测。如果您处于可能的情况下应该存在另一个匹配的实例,但由于代码中的错误而无法进行(忘记导入模块例如,在它定义的地方,它不会提交它可以看到的唯一匹配实例。它仅在知道时才选择一个实例。 2

所以"只有TT t = Maybe Int"争论不让GHC向后工作以解决t可能是Int

一般来说,只有类型系列,你才可以向前工作&#34 ;;如果您知道您应用类型系列的类型,您可以从中确定结果类型应该是什么,但如果您知道结果类型,则不会识别输入类型。这通常是令人惊讶的,因为普通类型的构造函数让我们向后工作"这条路;我们使用上述内容从Maybe _b = Maybe Int _b = Int得出结论。这只能起作用,因为使用新的data声明,应用类型构造函数总是在结果类型中保留参数类型(例如,当我们将Maybe应用于Int时,结果类型为{{1 }})。相同的逻辑不适用于类型族,因为可能有多个类型族实例映射到相同的类型,即使没有,也没有要求可识别的模式连接输入类型的结果类型中的某些内容(我可以Maybe Int

因此,您经常会发现,当您需要添加类型注释时,您经常会发现在类型为类型族的结果的地方添加一个注释并不起作用,而且您需要将输入固定到类型系列(或者需要与其类型相同的其他类型)。

1 请注意,您在问题type TT Char = Maybe (Int -> Double, Bool)中引用的行实际上并不起作用。 main = putStrLn $ show $ doIt (Just 2) 25::Int签名尽可能地绑定"",因此您实际上声称整个表达式:: Int属于putStrLn $ show $ doIt (Just 2) 25类型,必须时属于Int类型。我假设您确实在检查时将IO ()括起来,25 :: Int

2 关于GHC所考虑的内容,有一些特定的规则"某些知识"不可能有任何其他匹配的实例。我没有详细介绍它们,但基本上当你有putStrLn $ show $ doIt (Just 2) (25 :: Int)时,它必须能够仅通过考虑instance Constraints a => SomeClass (T a)位来明确地选择一个实例;它无法查看SomeClass (T a)箭头左侧的约束。