2
votes

I am having trouble with ambiguous types in Haskell. I started out with the following:

module GameState
( GameState(..)
, GameStateMonad
, module Control.Monad.Trans
, module Control.Monad.Trans.State.Lazy
, Blank(..)
) where

import Control.Monad.Trans
import Control.Monad.Trans.State.Lazy

type GameStateMonad a b = StateT a IO b

class GameState a where
    update :: Double -> GameStateMonad a ()
    update deltaTime = return ()

    draw :: GameStateMonad a ()
    draw = return ()

    getNextState :: GameState b => GameStateMonad a (Maybe b)
    getNextState = return Nothing

    isStateFinished :: GameStateMonad a Bool
    isStateFinished = return True

-- This is just a dummy data and instance declaration to demonstrate the error
data Blank = Blank
instance GameState Blank

Then when I try to run the following in ghci:

runStateT getNextState Blank

I get:

Ambiguous type variable `b0' in the constraint:
  (GameState b0) arising from a use of `getNextState'
Probable fix: add a type signature that fixes these type variable(s)
...

I thought it was complaining that my default implementation of the getNextState function didn't specify a concrete type, so I tried the following:

getNextState :: GameState b => GameStateMonad a (Maybe b)
getNextState = return (Nothing :: Maybe Blank)

Unfortunately I got this error while compiling:

Could not deduce (b ~ Blank)
from the context (GameState a)
  bound by the class declaration for `GameState'
  at GameState.hs:(14,1)-(25,33)
or from (GameState b)
  bound by the type signature for
             getNextState :: GameState b => GameStateMonad a (Maybe b)
  at GameState.hs:22:5-50
  `b' is a rigid type variable bound by
      the type signature for
        getNextState :: GameState b => GameStateMonad a (Maybe b)
      at GameState.hs:22:5
...

But I found that adding a type signature when I call getNext state allows the code to run:

runStateT (getNextState :: GameStateMonad Blank (Maybe Blank)) Blank

Unfortunately this stops me from making generic code to handle game states. It also makes little sense to me. What's the point in returning a polymorphic type if you have to give it an explicit type after it's returned? The original issue is also really confusing to me because I can make a function as follows:

test :: Num a => Maybe a
test = Nothing

And have no problems running it. Shouldn't this complain about ambiguous types like my original code? Also when giving the return value an explicit type I cannot compile it, like before:

test :: Num a => Maybe a
test = Nothing :: Maybe Int

I don't see why this is a problem. Int is an instance of type Num so the type of the function is correct.

I have four questions:

  1. Why does giving an explicit type when returning an element of a typeclass cause a compile error?

  2. Why does returning an ambiguous Maybe value inside of getNextState cause an error but inside test it does not?

  3. Why does this error occur without me calling a function on the returned polymorphic data, as explained here?

  4. In the link above, the answer mentions that "[you get this error] because you have something that produces a polymorphic result, then apply a function that takes a polymorphic argument to that result, such that the intermediate value's type is unknown". Doesn't this mean that functions that return a polymorphic result are essentially useless?

Thanks.

2

2 Answers

6
votes

Cat Plus Plus has already explained why

getNextState :: GameState b => GameStateMonad a (Maybe b)
getNextState = return (Nothing :: Maybe Blank)

doesn't work, so I can be short on that. The type signature promises that getNextState can deliver a value of type Maybe b for whatever type b the caller demands. It's the caller of the function who decides what type it shall return if a function has polymorphic return type. So the signature promises "whatever you want, as long as it's a GameState instance", but the implementation says "No, I don't care what you ordered, I return a Blank".

Ambiguous type variable `b0' in the constraint:
  (GameState b0) arising from a use of `getNextState'
Probable fix: add a type signature that fixes these type variable(s)

from typing

runStateT getNextState Blank

at the ghci prompt. If you ask ghci for the type of that, it will tell you

runStateT getNextState Blank :: GameState b => IO (Maybe b)

(no guarantees about the choice of type variable). But there is no context, so ghci doesn't know which type to instantiate b with. And so it doesn't know which implementation of getNextState it should call [or, which dictionary it should pass, if we look at GHC's implementation of type classes]. It has no way of resolving that ambiguity, so it tells you about it and suggests how you could resolve it.

test :: Num a => Maybe a
test = Nothing

Yes, that's in principle the same problem, when you type test at the ghci prompt. But there are special rules for resolving ambiguous type variables when there is one numeric class involved (where ambiguities are most frequent, literals are already ambiguous), and all involved constraints are simple and concern classes of the Prelude or the standard libraries. In that case, ambiguous type variables are instantiated by defaulting, so ghci would choose to instantiate a with Integer and print the Nothing of the type Maybe Integer.

Your GameState class is not defaultable, that's the difference between these examples.

Why does this error occur without me calling a function on the returned polymorphic data, as explained here?

Because you're not calling any function that would determine the type. If you had a function of type

foo :: Blank -> Int

and typed

runStateT getNextState Blank >>= print . maybe 0 foo

the use of foo would determine b and all would be swell.

The problem would however not be solved but exacerbated, if you called a polymorphic function (whose argument type cannot be inferred from its result type), as in the linked example. Then the ambiguous type is no longer reachable from the outside, and it can never be resolved. Then the only way is to provide a type signature that resolves the ambiguity.

Doesn't this mean that functions that return a polymorphic result are essentially useless?

Oh no, they are immensely useful. Look at read, or fromInteger, realToFrac, ...

The point is, that the type at which they shall be used must somehow be determined where they are used. Most of the time that is done by the calling context, but sometimes an explicit type signature is necessary.

3
votes

I'm not sure what you're trying to achieve by making GameState a typeclass. You might be too much in OOP mindset here — typeclasses are not OOP classes. A set of game states will probably be closed, so it might make more sense to just make it a single ADT.

Why does giving an explicit type when returning an element of a typeclass cause a compile error?

Look at your function's signature: GameState b => GameStateMonad a (Maybe b). Now remember that this implies forall b. GameState b => GameStateMonad a (Maybe b).

Implementation of the function cannot decide what b is — it must work for all of them (as long as they fit in constraints), because it's up for the caller to decide.

That's why return (Nothing :: Maybe Blank) is an error — it doesn't work for all types b — it only works for Blank. "Could not deduce (b ~ Blank)" means GHC can't prove type equality here. Same thing goes for Nothing :: Maybe Int. And that's also why adding type signature in the call site works.

I'll write something on ambiguities later maybe. I'm still pretty sure you're overengineering this code anyway, so the solution is to not do that.