Generic type vs rigid type in Haskell function

Viewed 88

Why ghc doesn't complain about types being rigid in the following function?

play :: (Monad m, MonadIO m, Random a) => a -> a -> m a
play r1 r2 = do
  randomRIO (r1, r2)

In the above case, the m is actually IO. This code compiles. While othertimes, when I use a concrete type in a generic function, ghc complains that I am using a concrete type. Do I miss something here?

For example, if I say:

play :: (Monad m, MonadIO m, Random a) => a -> a -> m a
play r1 r2 = do
  randomRIO (1, 6::Int)

This time ghc complains that I'm using a rigid type. The thing that bothers me is that IO is in a way a concrete or rigid type because it's IO and not Maybe.

Edit 1: Complete code that compiles without error:

{-# LANGUAGE ConstraintKinds #-}
module Main where

-- showing how to program in the mtl style
import           Control.Monad.IO.Class
import           System.Random

type Game m a = (Monad m, MonadIO m, Random a, Show a)

-- step 1
play :: (Game m a) => a -> a -> m a
play r1 r2 = do
  yell "hi"
  randomRIO (r1, r2)
  --rollDice r1 r2

-- step 2
yell :: (MonadIO m) => String -> m ()
yell str = liftIO $ putStrLn str

-- step 3
rollDice :: (Game m a) =>  a -> a ->  m a
rollDice a1 a2 = liftIO $ randomRIO (a1,a2)

main :: IO ()
main = putStrLn ""
-- main :: IO ()
-- main = do
--   play 1 (6::Int) >>= print
--   play 'a' 'z' >>= print
--   putStrLn "done"

Code that doesn't compile:

{-# LANGUAGE ConstraintKinds #-}
module Main where

-- showing how to program in the mtl style
import           Control.Monad.IO.Class
import           System.Random

type Game m a = (Monad m, MonadIO m, Random a, Show a)

-- step 1
play :: (Game m a) => a -> a -> m a
play r1 r2 = do
  yell "hi"
  randomIO 'a' 'z'
  --rollDice r1 r2

-- step 2
yell :: (MonadIO m) => String -> m ()
yell str = liftIO $ putStrLn str

-- step 3
rollDice :: (Game m a) =>  a -> a ->  m a
rollDice a1 a2 = liftIO $ randomRIO (a1,a2)

main :: IO ()
main = putStrLn ""
-- main :: IO ()
-- main = do
--   play 1 (6::Int) >>= print
--   play 'a' 'z' >>= print
--   putStrLn "done"

This doesn't compile:

{-# LANGUAGE ConstraintKinds #-}
module Main where

-- showing how to program in the mtl style
import           Control.Monad.IO.Class
import           System.Random

type Game m a = (Monad m, MonadIO m, Random a, Show a)

-- step 1
play :: (Game m a) => a -> a -> m a
play r1 r2 = do
  yell "hi"
  **randomRIO (1, 6::Int)**
  --rollDice r1 r2

-- step 2
yell :: (MonadIO m) => String -> m ()
yell str = liftIO $ putStrLn str

-- step 3
rollDice :: (Game m a) =>  a -> a ->  m a
rollDice a1 a2 = liftIO $ randomRIO (a1,a2)

main :: IO ()
main = putStrLn ""
-- main :: IO ()
-- main = do
--   play 1 (6::Int) >>= print
--   play 'a' 'z' >>= print
--   putStrLn "done"

Update

Just realized that my confusion comes from reading the wrong documentation of random I was reading the random-1.1 documentation while using the random-1.2 in my code.

For people's reference:

random-1.1
randomRIO :: (a, a) -> IO a

random-1.2
randomRIO :: (Random a, MonadIO m) => (a, a) -> m a  
1 Answers

I guess you are using random-1.2, which uses a more general type for randomRIO.

In that updated package, randomRIO is polymorphic both on the monad m and the value type a. Its type is:

randomRIO :: (Random a, MonadIO m) => (a, a) -> m a 

So, when you write (renaming type variables for clarity)

play :: (Monad m1, MonadIO m1, Random a1) => a1 -> a1 -> m1 a1
play r1 r2 = do
  randomRIO (r1, r2)

GHC infers m a ~ m1 a1, which implies m ~ m1 and a ~ a1, so randomRIO is called with those type parameters (m and a are not rigid here). Note that IO is not involved here at all: m could be IO, but could also be any other monad (in the MonadIO class).

Instead, when you write

play :: (Monad m1, MonadIO m1, Random a1) => a1 -> a1 -> m1 a1
play r1 r2 = do
  randomRIO (1, 6::Int)

GHC infers once again m a ~ m1 a1, hence m ~ m1 and a ~ a1, but also infers a ~ Int since we are passing a pair of Ints to randomRIO. From a ~ a1 and a ~ Int we infer a1 ~ Int which triggers the type error since a1 is rigid.

If we instead had only a more specific type for randomRIO (such as the one we had prior to random-1.2)

randomRIO :: (Random a) => (a, a) -> IO a

then your reasoning would be correct: we would get m1 ~ IO during type inference, and that would cause an error since m1 is rigid. We don't get this since randomRIO is more general.

Related