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

haw*_*eye 7 haskell ghci

我正在学习下面的书来学习Haskell - 特别是关于随机性的章节:

我正在运行以下文件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) )
Run Code Online (Sandbox Code Playgroud)

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

(True,True,True)
Run Code Online (Sandbox Code Playgroud)

现在他们在笔记中说明了一点:

请注意,我们没有必要这样做random gen :: (Bool, StdGen).那是因为我们已经指定我们在函数的类型声明中需要布尔值.这就是为什么Haskell可以推断出在这种情况下我们想要一个布尔值.

太棒了.

现在当我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)
:}
Run Code Online (Sandbox Code Playgroud)

我收到以下回复:

<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)
Run Code Online (Sandbox Code Playgroud)

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

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

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)
:}
Run Code Online (Sandbox Code Playgroud)

工作正常 - 我们可以用以下方法测试它:

threeCoins (mkStdGen 21) 
Run Code Online (Sandbox Code Playgroud)

并得到这个结果

(True,True,True)
Run Code Online (Sandbox Code Playgroud)

嗯 - 那很有效.所以Haskell编译器可以从我们提供的类型推断出我们想要一个布尔值,但是ghci不能.

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

lef*_*out 3

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

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

从概念上讲firstCoin secondCoin thirdCoin等也可以比Bool:更通用random,毕竟能够产生任何合适类型的随机值(即具有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)
Run Code Online (Sandbox Code Playgroud)

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

{-# LANGUAGE NoMonomorphismRestriction #-}
Run Code Online (Sandbox Code Playgroud)

在上面。

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

最好的解决方案是不要进行手动元组展开,这无论如何都很尴尬,而是使用随机 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
Run Code Online (Sandbox Code Playgroud)

无论有或没有单态限制,它都可以工作,因为firstCoin secondCoin thirdCoin现在来自一元绑定,始终是单态的

顺便说一句,因为你在一个 monad 中,所以你可以使用标准组合器,从而轻松地将其缩短为

import Control.Monad (replicateM)

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