Skip to content

does not work: newtype D = D Double deriving UniformRange #185

Description

@jwaldmann

I have a newtype over Double, I want to use the built-in Double generator, but I get this error.

ghci> :set -package random
package flags have changed, resetting and loading new packages...
ghci> import System.Random
ghci> :set -XGeneralizedNewtypeDeriving 
ghci> newtype D = D Double deriving Uniform
Uniform       UniformRange
ghci> newtype D = D Double deriving UniformRange 
<interactive>:5:31: error: [GHC-18872]
    • Couldn't match representation of type: m Double
                               with that of: m D
        arising from the coercion of the method ‘System.Random.Internal.uniformRM’
          from type ‘forall g (m :: * -> *).
                     System.Random.Internal.StatefulGen g m =>
                     (Double, Double) -> g -> m Double’
            to type ‘forall g (m :: * -> *).
                     System.Random.Internal.StatefulGen g m =>
                     (D, D) -> g -> m D’
      Note: We cannot know what roles the parameters to ‘m’ have;
            we must assume that the role is nominal.
    • When deriving the instance for (UniformRange D)

I can write the instance just fine

import qualified System.Random as R
import qualified System.Random.Stateful as RS

instance R.UniformRange F where
  uniformRM (F l, F h) g = fmap F $ RS.uniformRM (l, h) g
  isInRange (F l, F h) (F x) = RS.isInRange (l, h) x

but that's precisely what I wanted to avoid. It looks completely generic so the compiler should be able to provide/derive it?

Activity

  1. lehins commented on Apr 23, 2025

    @lehins
    Contributor

    It is a known limitation, which is why we also do not using deriving mechanism for newtype wrappers in random, eg:

    instance UniformRange CDouble where
    uniformRM (CDouble l, CDouble h) = fmap CDouble . uniformRM (l, h)
    {-# INLINE uniformRM #-}
    isInRange = isInRangeOrd

    The problem is that you cannot use GeneralizedNewtypeDeriving to derive a type class instance that has a function with a nominal role, which polymorphic types have, since they are unknown

    Let me provide a slightly simpler example. Let's say we have a function that converts Int to Word safely:

    int2word :: Int -> Either String Word
    int2word i =
      if i < 0
        then Left "Word can't be negative"
        else Right $ fromIntegral i

    And we also have a general interface like this that can convert Int to other types safely and an instance that uses above implementation:

    class PolyInt a where
      polyInt :: MonadFail m => Int -> m a
    
    instance PolyInt Word where
      polyInt = either fail pure . int2word

    Because result of polyInt has m a, it is impossible for the compiler to provide an instance using GeneralizedNewtypeDeriving that uses coercion underneath, because m in m a is not known to be coercible. Hence if we try to make a derived instance:

    newtype MyWord = MyWord Word
      deriving PolyInt

    we'll get a type checker error, that essentially says the same thing as I did in more concise way:

       • Couldn't match representation of type: m Word
                                   with that of: m MyWord
            arising from the coercion of the method ‘polyInt’
              from type ‘forall (m :: * -> *). MonadFail m => Int -> m Word’
                to type ‘forall (m :: * -> *). MonadFail m => Int -> m MyWord’
            NB: We cannot know what roles the parameters to ‘m’ have;
              we must assume that the role is nominal
        • When deriving the instance for (PolyInt MyWord)

    In this particular example we could work around this limitation, by making the type class more stringent:

    class MonoInt a where
      monoInt :: Int -> Either String a
    
    polyInt' :: (MonadFail m, MonoInt a) => Int -> m a
    polyInt' = either fail pure . monoInt
    
    instance MonoInt Word where
      monoInt = int2word
    
    newtype MyWord = MyWord Word
      deriving (MonoInt)

    We can do this because Either Stringcaptures the essence of MonadFail.

    Unfortunately, I do not see a way to do something like that for StatefulGen, since we actually rely on different behavior of the m for producing random numbers.

    That being said, I don't think this is a severe limitation of the interface, considering the generality we get from stateful generators.

    Here is another example of a similar problem from ghc issue tracker that goes into some more explanation, if you want to learn more about it: https://gitlab.haskell.org/ghc/ghc/-/issues/21957

  2. jwaldmann commented on Apr 23, 2025

    @jwaldmann
    Author

    Thanks, I see, that seems to be a shortcoming of the type system (missing "higher order roles").

    Perhaps it is possible to add to the docs a recommended way of writing instances (for newtypes). I see that there is such a hint for Finite already. By copying that, I get

    ghci> newtype B = B Bool deriving (Show, Generic, Random, Uniform, UniformRange)
    ghci> random @B @StdGen <$> getStdGen
    (B True,StdGen {unStdGen = SMGen 6891688420623091382 2170343918981675385})
    

    OK, works. This does not:

    ghci> newtype D = D Double deriving (Show, Generic, Random, Uniform, UniformRange)
    <interactive>:12:55: error: [GHC-39999]
        • Could not deduce ‘Uniform Double’
    

    OK, that's because Random is considered legacy (? - the default impl. requires Uniform which Double does not have, with good reason). This works:

    ghci> newtype D = D Double deriving (Show, Generic,  UniformRange)
    ghci> uniformR  @D @StdGen (D 0, D 1) <$> getStdGen
    

    So that solves my immediate problem.

    Still, documentation could be improved (I can make a separate issue)

    • "introduction" section does mention RandomGen, should also mention Uniform
    • "usage" section shows a method from Uniform, should also mention the class
    • later section "how to implement RandomGen" is fine, should also have "how to implement Uniform" somewhere (as that would be the much more frequent use case?)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions