为什么我们不能在 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)

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

然后我在网上搜索并找到了这个:

请为随机 #21 提供(枚举 a,有界 a(的实例

一些引号似乎回答了我的问题,只是我不太明白:

你想到什么实例,实例(有界 a,枚举 a(=>随机 a 在哪里...?不可能有这样的实例,因为它会 与所有其他实例重叠。

这将阻止任何用户派生实例。

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

为什么这行不通?

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的内置实例重叠。 (IntBoundedEnum,这实际上是一个巧合——它也将与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,但它永远不会发生。

同样,可以允许为任何EnumBounded的类型自动派生Random,但社区不愿意添加额外的自动派生机制,特别是对于像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

在您自己的代码中。

最新更新