为什么在Haskell中不能为枚举派生随机类实例?

时间:2018-08-12 18:42:29

标签: haskell enums typeclass deriving

我今天写的是这个

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

一些引号似乎可以回答我的问题,除了我不太了解它们之外:

  

您要记住什么实例,实例(边界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)

1 个答案:

答案 0 :(得分:5)

评论指的是这样的事实:在Haskell中(实际上在扩展名为FlexibleInstances的Haskell中),实例匹配是通过在不考虑约束的情况下匹配类型来完成的。 之后,类型匹配成功,然后检查约束,如果不满足约束,则会生成错误。因此,如果您定义:

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

您实际上是为每种类型a定义一个实例,而不仅仅是具有aBounded实例的类型Enum。就像您写过一样:

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的内置实例重叠,两者都不是。)< / p>

无论如何,您可以通过使实例可重叠来解决此问题,从而实现以下目的:

{-# 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的任何类型的 是可能的,但是社区不愿意添加额外的自动派生机制,尤其是对于RandomShow这样不常用的类型类。因此,再也不会发生。

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

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

使用您自己的代码。