"Could not deduce" errors when mixing MonadIO, MonadState, and zoom

Viewed 296

I was working on a simple monte carlo simulation and I ran into this problem. I was using MonadIO, MonadState, and MonadRandom to simplify maintaining the state of my program. I was getting Count not deduce errors, but when I removed the type of the top level monad of my program, all of a sudden it compiled just fine.

Am I doing something wrong with this type? Do I need to specify a tighter type? Can you explain why adding type information can cause type calculations to be more ambiguous instead of less ambiguous?

UPDATE

I tried using :t in ghci to get a type and I got (Field1 s s Int Int, Zoom m n Int s, Functor (Zoomed m ())). I did the same in my original program and got (Zoom m1 m (Map Int Int) ProgramState, Functor (Zoomed m1 ()), MonadRandom m1, MonadIO m)

I'm happy to have something here, but as the main point of putting in a type is to make the code more readable and to make error messages better, I'd like to understand how to generate and read these types.

I'd also like to understand better how adding a type constraint can cause type calculations to become ambiguous.

END UPDATE

I've simplified the code:

{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.State.Strict
import Control.Lens

updateCount :: (MonadState Int a) => a ()
updateCount = modify (+ 1)

run :: MonadState (Int, Int) a => a ()
run = zoom _1 updateCount

main = execStateT run (0, 0) >>= print

If I remove the run :: line, everything works fine, but if I try to compile as is, I get the following errors:

[1 of 1] Compiling Main             ( bug-report.hs, bug-report.o )

bug-report.hs:9:7: error:
    • Could not deduce (Zoom m0 a Int (Int, Int))
        arising from a use of ‘zoom’
      from the context: MonadState (Int, Int) a
        bound by the type signature for:
                   run :: MonadState (Int, Int) a => a ()
        at bug-report.hs:8:1-38
      The type variable ‘m0’ is ambiguous
      Relevant bindings include run :: a () (bound at bug-report.hs:9:1)
      These potential instances exist:
        instance Monad z => Zoom (StateT s z) (StateT t z) s t
          -- Defined in ‘Control.Lens.Zoom’
        ...plus 12 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In the expression: zoom _1 updateCount
      In an equation for ‘run’: run = zoom _1 updateCount

bug-report.hs:9:12: error:
    • Could not deduce (Functor (Zoomed m0 ()))
        arising from a use of ‘_1’
      from the context: MonadState (Int, Int) a
        bound by the type signature for:
                   run :: MonadState (Int, Int) a => a ()
        at bug-report.hs:8:1-38
      The type variable ‘m0’ is ambiguous
      These potential instances exist:
        instance Functor Identity -- Defined in ‘Data.Functor.Identity’
        instance Functor IO -- Defined in ‘GHC.Base’
        instance [safe] Functor m => Functor (StateT s m)
          -- Defined in ‘Control.Monad.Trans.State.Strict’
        ...plus four others
        ...plus 41 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In the first argument of ‘zoom’, namely ‘_1’
      In the expression: zoom _1 updateCount
      In an equation for ‘run’: run = zoom _1 updateCount

bug-report.hs:9:15: error:
    • Could not deduce (MonadState Int m0)
        arising from a use of ‘updateCount’
      from the context: MonadState (Int, Int) a
        bound by the type signature for:
                   run :: MonadState (Int, Int) a => a ()
        at bug-report.hs:8:1-38
      The type variable ‘m0’ is ambiguous
      These potential instances exist:
        instance [safe] Monad m => MonadState s (StateT s m)
          -- Defined in ‘Control.Monad.State.Class’
        ...plus 13 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In the second argument of ‘zoom’, namely ‘updateCount’
      In the expression: zoom _1 updateCount
      In an equation for ‘run’: run = zoom _1 updateCount
2 Answers

Zoom doesn’t let you work polymorphically over MonadState in general because MonadState alone doesn’t provide a way to swap out the type of state.

That is, if you want to use Zoom, you should probably be using a concrete StateT, if you don’t need polymorphism over StateT specifically and can get away with polymorphism only over the underlying monad (i.e. the m in StateT s m). Alternatively, you could skip zoom and still use lenses to focus on portions of the state for pure functions, with combinators like modifying—here, modifying _1 (+ 1).

When you remove the type signature of run, the inferred type (with NoMonomorphismRestriction) is this:

run ::
  ( Zoom m n Int s
  , Field1 s s Int Int
  , Functor (Zoomed m ())
  )
  => n ()

That is:

  • An action in some outer monad n whose state is s (here, (Int, Int))

  • Wrapping an action in some inner monad m whose state is Int

  • Where you can view & update an Int at the first index of s

  • And Zoomed m () is a Functor, where the type family application Zoomed (StateT s u) () evaluates to Focusing u (); this is required due to the Functor constraint in the type of a lens, Functor f => (a -> f b) -> (s -> f t)

The Functor constraint is necessary here because the compiler can’t deduce it until the type family Zoomed is applied to a concrete m. Notice that you don’t need a MonadState constraint, though, because it’s implied by Zoom.

You can simplify this slightly in a few different ways, depending on your particular use case. For example, using the concrete outer state:

run ::
  ( Zoom m n Int (Int, Int)
  , Functor (Zoomed m ())
  )
  => n ()

Or passing the responsibility to the caller and taking a Lens:

runOn
  :: (Zoom m n Int s, Functor (Zoomed m ()))
  => LensLike' (Zoomed m ()) s Int
  -> n ()
runOn lens = zoom lens updateCount
print =<< execStateT (runOn _1) (0 :: Int, 0 :: Int)

The error messages include a lot of information, but the first thing they say is the most important: Could not deduce (Zoom m0 a Int (Int, Int)) arising from a use of ‘zoom’. That's telling you that using zoom polymorphically requires an extra class constraint. If you just add that to the list of constraints on run, it should be enough in this case.

Sometimes the solution is more complex, but this time the problem is just that run has a type that's less constrained than it actually needs to be in order to work.

Related