为什么我们不能为 Haskell 中的枚举派生随机 class 实例?

Why can't we have Random class instances derived for enumerations in Haskell?

我今天写了这个:

data Door = A | B | C
 deriving (Eq,Bounded,Enum)

instance Random Door where
 randomR (lo,hi) g = (toEnum i, g')
  where (i,g') = randomR (fromEnum lo, fromEnum hi) g
 random = randomR (minBound,maxBound)

而且我认为这对于任何枚举来说都是大致可复制粘贴的。 我尝试将 Random 放入派生子句中,但失败了。

然后我在网上搜索了一下,发现了这个:

Please provide instance for (Enum a, Bounded a) for Random #21

一些引述似乎回答了我的问题,但我不太理解它们:

What instance do you have in mind, instance (Bounded a, Enum a) => Random a where ...? There can't be such an instance, since it would overlap with every other instance.

This would prevent any user derived instances. ...

为什么这不能通过派生子句或至少通过默认实现实现自动化。

为什么这行不通?

instance (Bounded a, Enum a) => Random a where
   randomR (lo,hi) g = (toEnum i, g')
       where (i,g') = randomR (fromEnum lo, fromEnum hi) g
   random = randomR (minBound,maxBound)

评论是指在Haskell中(实际上是在Haskell中扩展FlexibleInstances),实例匹配是通过匹配类型来完成的,而不考虑约束。 类型匹配成功后,将检查约束,如果不满足约束将产生错误。所以,如果你定义:

instance (Bounded a, Enum a) => Random a where ...

您实际上是在为每个类型 a 定义一个实例,而不仅仅是具有 BoundedEnum 实例的类型 a。就好像你写了:

instance Random a where ...

这将可能与任何其他库定义或用户定义的实例发生冲突,例如:

newtype Gaussian = Gaussian Double
instance Random Gaussian where ...

有很多方法可以解决这个问题,但整个事情最终会变得非常混乱。此外,它可能会导致一些神秘的编译类型错误消息,如下所述。

具体来说,如果将以下内容放入模块中:

module RandomEnum where

import System.Random

instance (Bounded a, Enum a) => Random a where
   randomR (lo,hi) g = (toEnum i, g')
       where (i,g') = randomR (fromEnum lo, fromEnum hi) g
   random = randomR (minBound,maxBound)

您会发现您需要 FlexibleInstances 扩展来允许对实例的限制。很好,但是如果您添加它,您将看到您需要 UndecidableInstances 扩展名。这可能不太好,但如果你添加它,你会发现你在 randomR 定义的 RHS 上调用 randomR 时出错。 GHC 已确定您定义的实例现在与 Int 的内置实例重叠。 (Int 既是 Bounded 又是 Enum 实际上是个巧合——它也会与 Double 的内置实例重叠,但两者都不是。)

无论如何,您可以通过使您的实例可重叠来解决这个问题,因此:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}

module RandomEnum where

import System.Random

instance {-# OVERLAPPABLE #-} (Bounded a, Enum a) => Random a where
   randomR (lo,hi) g = (toEnum i, g')
       where (i,g') = randomR (fromEnum lo, fromEnum hi) g
   random = randomR (minBound,maxBound)

将实际编译。

这基本上没问题,但您最终可能会收到一些奇怪的错误消息。通常,以下程序:

main = putStrLn =<< randomIO

会生成合理的错误消息:

No instance for (Random String) arising from a use of `randomIO'

但是有了上面的例子,它就变成了:

No instance for (Bounded [Char]) arising from a use of ‘randomIO’

因为您的实例匹配 String 但 GHC 找不到 Bounded String 约束。

无论如何,总的来说,Haskell 社区避免将这些类型的包罗万象的实例放入标准库中。事实上,他们需要 UndeciableInstances 扩展和 OVERLAPPABLE 编译指示,并可能在程序中引入一堆不需要的实例,这一切都在人们的口中留下了不好的印象。

因此,虽然在技术上可能将这样的实例添加到 System.Random,但它永远不会发生。

同样,可能允许Random自动派生为EnumBounded的任何类型,但是社区不愿意添加额外的自动派生机制,特别是对于类型 类,例如 Random,这些机制并不经常使用(与 ShowEq 相比)。所以,再一次,它永远不会发生。

相反,允许方便的默认实例的标准方法是定义一些可以在显式实例定义中使用的辅助函数,这就是您链接的提案底部所建议的。例如,可以在 System.Random 中定义以下函数:

defaultEnumRandomR :: (Enum a, RandomGen g) => (a, a) -> g -> (a, g)
defaultEnumRandomR (lo,hi) g = (toEnum i, g')
       where (i,g') = randomR (fromEnum lo, fromEnum hi) g

defaultBoundedRandom :: (Random a, Bounded a, RandomGen g) => g -> (a, g)
defaultBoundedRandom = randomR (minBound, maxBound)

人们会写:

instance Random Door where
    randomR = defaultEnumRandomR
    random = defaultBoundedRandom

这是唯一有机会进入 System.Random 的解决方案。

如果确实如此并且您不喜欢必须定义显式实例,您可以自由坚持:

instance {-# OVERLAPPABLE #-} (Bounded a, Enum a) => Random a where
    randomR = defaultEnumRandomR
    random = defaultBoundedRandom

在您自己的代码中。