4

I wrote this today:

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)

And I figured this is roughly copy-pastable for any enumeration. I tried putting Random into the deriving clause, but that failed.

Then I searched the web and found this:

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

A few quotes that seem to answer my question, except I don't quite understand them:

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. ...

Why can't this be automated, by either the deriving clause or with a default implementation at least.

Why wouldn't this work?

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)
jwodder
  • 54,758
  • 12
  • 108
  • 124
hipogumafe
  • 51
  • 2
  • Please note `instance (Bounded a, Enum a) => Random a` is not a valid instance declaration. See: [the specification](https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-630004.1). – AJF Aug 12 '18 at 19:43
  • 1
    That would overlap with any other instance, since Haskell does not check the context when considering overlaps (IIRC that would require an exptime blowup). Further, what if I wanted to use a different distribution for my custom enumerated type? One would need a sort-of priority between multiple overlapping instances. GHC has some of that, but it can be quite fragile. Overlapping (and incoherent) instances can be too unpredictable -- I'd avoid them as much as possible. – chi Aug 12 '18 at 21:52
  • @chi "Haskell does not check the context when considering overlaps" Isn't that check done for normal functions? Doesn't the algorithm "sort by increasing generality and choose the 1st that matches" work? That seems: nlogn sort + n checks. – hipogumafe Aug 13 '18 at 06:49
  • 1
    If, on a context failure, you need to check the next one in the list, you need backtracking, roughly as you would when running a Prolog program. Each of your "n checks" can trigger other "m checks", which can trigger ... etc. Haskell checks the instance head of all instances, but the instance context of only one, so that recursively we get n+m+... instead of n*m*.... The actual algorithm is complex, I think you can find it in the GHC manual looking for overlapping instances. – chi Aug 13 '18 at 08:19

1 Answers1

7

The comments are referring to the fact that in Haskell (actually in Haskell with the FlexibleInstances extension), instance matching is done by matching the type without considering the constraints. After the type match succeeds, the constraints are then checked and will generate errors if they aren't satisfied. So, if you define:

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

you are actually defining an instance for every type a, not just types a that have Bounded and Enum instances. It's as if you had written:

instance Random a where ...

This will then potentially conflict with any other library-defined or user-defined instances, such as:

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

There are ways to get around this, but the whole thing ends up getting pretty messy. Also, it can lead to some mysterious compile-type error messages, as noted below.

Specifically, if you put the following in a module:

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)

you'll find that you need the FlexibleInstances extension to allow constraints on instances. That's fine, but if you add that, you'll then see that you need the UndecidableInstances extension. That's maybe less fine, but if you add that, you'll find that you then get an error on the randomR call on the RHS of your randomR definition. GHC has determined that the instance you have defined now overlaps with the built-in instance for Int. (It's actually a coincidence that Int is both Bounded and Enum -- it would have also overlapped with the built-in instance for Double, which is neither.)

Anyway, you can get around this by making your instance overlappable, so that the following:

{-# 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)

will actually compile.

This is mostly fine, but you may end up with some bizarre error messages. Normally, the following program:

main = putStrLn =<< randomIO

would generate the sensible error message:

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

But with the above instance in place, it becomes:

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

because your instance matches String but GHC fails to find a Bounded String constraint.

Anyway, in general, the Haskell community has avoided putting these sorts of catch-all instances into the standard library. The fact that they need the UndeciableInstances extension and OVERLAPPABLE pragmas and potentially introduce a bunch of undesirable instances into a program all leave a bad taste in people's mouths.

So, while it might be technically possible to add such an instance to System.Random, it will never happen.

Likewise, it would be possible to allow Random to be automatically derived for any types that are Enum and Bounded, but the community is reluctant to add additional automatic deriving mechanisms, particularly for type classes like Random that just aren't that often used (compared to say Show or Eq). So, again, it will never happen.

Instead, the standard way of allowing convenient default instances is to define some helper functions that can be used in an explicit instance definition, and that's what's being suggested in the bottom of the proposal you linked. For example, the following functions could be defined in 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)

and people would write:

instance Random Door where
    randomR = defaultEnumRandomR
    random = defaultBoundedRandom

This is the only solution that has a chance of making it into System.Random.

If it does and you dislike having to define explicit instances, you'll be free to stick:

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

in your own code.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • Isn't it obvious that it should only match an instance decl if constraints match? In case of overlap the more spacialized (concrete type, instead of type-variable) wins. Isn't that obvious? – hipogumafe Aug 13 '18 at 06:37
  • 1
    With respect to point #1, obvious to you, maybe, but not to GHC's type checker. The issue has to do with the fact that, internally, the type checker can't represent "a type that isn't fully specified yet satisfies the following constraints". With respect to point #2, yes, with overlapping/overlappable instance enabled, though the overlap rules are somewhat complicated. – K. A. Buhr Aug 13 '18 at 13:58