为什么 haskell 编译器可以推断出这种类型而 ghci 不能?

Why can the haskell compiler infer this type but ghci cannot?

我正在处理 following book to learn Haskell - looking in particular at the chapter on randomness:

我运行将以下内容作为文件 three-coins.hs:

import System.Random 

threeCoins :: StdGen -> (Bool, Bool, Bool)  
threeCoins gen =   
    let (firstCoin, newGen) = random gen
        (secondCoin, newGen') = random newGen
        (thirdCoin, newGen'') = random newGen'
    in  (firstCoin, secondCoin, thirdCoin)

main = print ( threeCoins (mkStdGen 21) )

然后我用 runhaskell three-coins.hs 执行并得到类似于以下的输出:

(True,True,True)

现在他们在笔记中指出了这一点:

Notice that we didn't have to do random gen :: (Bool, StdGen). That's because we already specified that we want booleans in the type declaration of the function. That's why Haskell can infer that we want a boolean value in this case.

太棒了。

现在,当我在 ghci 中 运行 使用以下代码时:

import System.Random 

:{
threeCoins :: StdGen -> (Bool, Bool, Bool)
threeCoins gen =
    let (firstCoin, newGen) = random gen
        (secondCoin, newGen') = random newGen
        (thirdCoin, newGen'') = random newGen'
    in  (firstCoin, secondCoin, thirdCoin)
:}

我收到以下回复:

<interactive>:6:9: error:
    • Ambiguous type variable ‘t0’
      prevents the constraint ‘(Random t0)’ from being solved.
    • When checking that the inferred type
        newGen :: forall t. Random t => StdGen
      is as general as its inferred signature
        newGen :: StdGen
      In the expression:
        let
          (firstCoin, newGen) = random gen
          (secondCoin, newGen') = random newGen
          (thirdCoin, newGen'') = random newGen'
        in (firstCoin, secondCoin, thirdCoin)
      In an equation for ‘threeCoins’:
          threeCoins gen
            = let
                (firstCoin, newGen) = random gen
                (secondCoin, newGen') = random newGen
                ....
              in (firstCoin, secondCoin, thirdCoin)

这很有趣。有点像他们在书中警告我们的错误。

因此,如果我们修改代码以将类型提示放入:

import System.Random 

:{
threeCoins :: StdGen -> (Bool, Bool, Bool)
threeCoins gen =
    let (firstCoin, newGen) = random gen :: (Bool, StdGen)
        (secondCoin, newGen') = random newGen :: (Bool, StdGen)
        (thirdCoin, newGen'') = random newGen' :: (Bool, StdGen)
    in  (firstCoin, secondCoin, thirdCoin)
:}

效果很好 - 我们可以使用以下方法对其进行测试:

threeCoins (mkStdGen 21) 

得到这个结果

(True,True,True)

嗯 - 成功了。所以 Haskell 编译器可以从我们提供的类型推断出我们需要一个布尔值,但是 ghci 不能。

我的问题是:为什么 haskell 编译器可以推断出这种类型而 ghci 不能?

正如 chi 已经评论的那样,此代码仅在启用 monomorphism restriction 时有效。该限制使编译器为任何非函数定义选择 一个特定类型 ,即其中没有类型变量的签名,如 length :: [a] -> Int 中的 a。因此(除非您手动指定了本地签名)编译器在选择之前到处寻找这种类型可能是什么的提示。在您的示例中,它看到 firstCoin secondCoin thirdCoin 用于最终结果,在顶级签名中声明了 (Bool, Bool, Bool),因此它推断出所有硬币必须具有类型 Bool

在这样一个简单的示例中,这很好,但在现代 Haskell 中,您经常需要更通用的值,因此您可以在多个不同类型的上下文中使用它们或作为 Rank 的参数-2 功能。你总是可以通过给出明确的签名来实现这一点,但特别是在 GHCi 中这很尴尬(它通常被称为“可怕的 单态限制”),因此在几个版本前决定禁用它在 GHCi 中默认。

概念上firstCoin secondCoin thirdCoin 等也可能比 Bool 更通用:random 毕竟能够产生 any 合适类型的随机值(即任何具有 Random 实例的类型)。所以原则上,局部定义可以有一个多态类型,像这样:

threeCoins :: StdGen -> (Bool, Bool, Bool)  
threeCoins gen =   
    let firstCoin, secondCoin, thirdCoin :: Random r => r
        (firstCoin, newGen) = random gen
        (secondCoin, newGen') = random newGen
        (thirdCoin, newGen'') = random newGen'
    in  (firstCoin, secondCoin, thirdCoin)

这基本上就是关闭单态性限制时发生的情况,正如您通过使用以下行编译原始示例所看到的那样

{-# LANGUAGE NoMonomorphismRestriction #-}

在上面。

问题是,您的代码实际上不适用于那些通用的本地签名。原因有点复杂,基本上是 r 变量的类型信息必须传播回元组才能在 random 生成器中使用,并且出于我现在的原因也不明白,Hindley-Milner 类型系统做不到。

最好的解决方案是 执行此手动元组解包,这无论如何都很尴尬,而是使用 random monad,如下所示:

import System.Random 
import Data.Random 

threeCoins :: RVar (Bool, Bool, Bool)  
threeCoins = do   
    firstCoin <- uniform False True
    secondCoin <- uniform False True
    thirdCoin <- uniform False True
    return (firstCoin, secondCoin, thirdCoin)

main = print . sampleState threeCoins $ mkStdGen 21

在有或没有同态限制的情况下都有效,因为 firstCoin secondCoin thirdCoin 现在来自单子绑定,which is always monomorphic.

顺便说一句,因为你在 monad 中,所以你可以使用标准组合器,因此很容易将其缩短为

import Control.Monad (replicateM)

threeCoins :: RVar (Bool, Bool, Bool)  
threeCoins = do   
    [firstCoin,secondCoin,thirdCoin] <- replicateM 3 $ uniform False True
    return (firstCoin, secondCoin, thirdCoin)