我正在学习下面的书来学习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不能?
正如 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)