
The trick is to write the rank-2 type in the function that runs the monad, and leave the typeclasses skolemized. Here's an example: -- | Typeclass for monads that write or read to a network. Useful -- if you define operations that need to work for all such monads. -- You're expected to put extra constraints on h. class (Network g, Monad (m g n), Applicative (m g n), Functor (m g n)) => NetworkMonad m g n where -- | Unsafely converts an 'IO' operation that takes an 'AIG' as an -- argument into an operation in some 'NetworkMonad'. unsafeIOToNetwork :: (GEnv g -> IO a) -> m g n a ... class OpaqueNetwork g => Network g where -- * We cannot put NetworkMonad constraint on GNT g and GNQ g because we -- need to be able to put that constraint as a rank-2 monad. -- * This has a lot of "stuff" in it, maybe we'll split it up later. data GNode g :: * -- ^ phantom type -> * -- ^ data type data GNT g :: * -- ^ phantom type -> * -> * -- ^ monad data GNQ g :: * -- ^ phantom type -> * -> * -- ^ monad data GEnv g :: * one :: GNode g n zero :: GNode g n runNT :: (forall n. NetworkMonad GNT g n => GNT g n ()) -> g withNT :: g -> (forall n. NetworkMonad GNT g n => GNT g n ()) -> g There are numerous other problems with this route (can you see them from the sample code?) but I found this solution to be mostly acceptable. Cheers, Edward