How to define MonadUnliftIO instance for a newtype with a phantom type-variable?
Asked Answered
C

2

8

Related question - Is it safe to derive MonadThrow, MonadCatch, MonadBaseControl, MonadUnliftIO, etc? - where I had enabled, both - DeriveAnyClass and GeneralizedNewtypeDeriving to get the code to compile, but didn't bother looking at the ominous warnings. Now, that I am running my refactored code, it's throwing a runtime error:

No instance nor default method for class operation >>=

So, I removed DeriveAnyClass and kept ONLY GeneralizedNewtypeDeriving and have the following compile error:

{-# LANGUAGE DataKinds, GADTs, ScopedTypeVariables, TypeFamilies, AllowAmbiguousTypes, RankNTypes, StandaloneDeriving, UndecidableInstances #-}

newtype AuthM (fs :: [FeatureFlag]) auth m a =
  AuthM (ReaderT (Auth auth) m a)
  deriving (Functor, Applicative, Monad, MonadReader (Auth auth), MonadIO, MonadThrow, MonadCatch, MonadMask, MonadUnliftIO)


--     • Couldn't match representation of type ‘m (Control.Monad.IO.Unlift.UnliftIO
--                                                   (AuthM fs auth m))’
--                                with that of ‘m (Control.Monad.IO.Unlift.UnliftIO
--                                                   (ReaderT (Auth auth) m))’
--         arising from the coercion of the method ‘Control.Monad.IO.Unlift.askUnliftIO’
--           from type ‘ReaderT
--                        (Auth auth)
--                        m
--                        (Control.Monad.IO.Unlift.UnliftIO (ReaderT (Auth auth) m))’
--             to type ‘AuthM
--                        fs auth m (Control.Monad.IO.Unlift.UnliftIO (AuthM fs auth m))’
--       NB: We cannot know what roles the parameters to ‘m’ have;
--         we must assume that the role is nominal
--     • When deriving the instance for (MonadUnliftIO (AuthM fs auth m))
--    |
-- 82 |   deriving (Functor, Applicative, Monad, MonadReader (Auth auth), MonadIO, MonadThrow, MonadCatch, MonadMask, MonadUnliftIO)
--    |                                                                                                               ^^^^^^^^^^^^^

Note: I realise that the first error about >>= has got nothing to do with the error about MonadUnliftIO. I have confirmed that there are no warnings about a missing >>=, when DeriveAnyClass is turned off.

I guess I need to write the instance for MonadUnliftIO myself, because the compiler probably cannot figure this out in the presence of a newtype AND a phantom type-variable. However, I just can't figure out how to define the askUnliftIO for my type, given above.

Attempt 1 at minimal code snippet

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Try13 where

import Control.Monad.Reader
import UnliftIO
import Control.Monad.Catch

data Auth = Auth

newtype AuhM m a = AuthM (ReaderT Auth m a)
  deriving(Functor, Applicative, Monad, MonadReader Auth, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadUnliftIO)

--     • Couldn't match representation of type ‘m (UnliftIO (AuhM m))’
--                                with that of ‘m (UnliftIO (ReaderT Auth m))’
--         arising from the coercion of the method ‘askUnliftIO’
--           from type ‘ReaderT Auth m (UnliftIO (ReaderT Auth m))’
--             to type ‘AuhM m (UnliftIO (AuhM m))’
--       NB: We cannot know what roles the parameters to ‘m’ have;
--         we must assume that the role is nominal
--     • When deriving the instance for (MonadUnliftIO (AuhM m))
--    |
-- 12 |   deriving(Functor, Applicative, Monad, MonadReader Auth, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadUnliftIO)
--    |                                                                                                       ^^^^^^^^^^^^^
-- 
Corr answered 25/7, 2019 at 9:34 Comment(4)
Could you provide the definitions of Auth and FeatureFlag, or simplify this code to the minimal part that reproduces this issue? Right now it's impossible to easily reproduce the error you're getting, and I'm sure you could cut out a few irrelevant parts.Cuspidor
@BartekBanachewicz I have added a minimal code snippet to the question. As a first step, I was able to get the error with just the newtype (i.e. without phantom-type).Corr
Not related to your question, but I think you newtype might be misspelled AuhM vs AuthM?Alcove
@Alcove yes, good catch :)Corr
G
8

Plan:

  • How to implement MonadUnliftIO by hand.
  • How to newtype-derive MonadUnliftIO.

Implement explicitly

newtype AuthM m a = AuthM { unAuthM :: ReaderT Auth m a }
  deriving ...

instance MonadUnliftIO m => MonadUnliftIO (AuthM m) where
  askUnliftIO = AuthM (fmap (\(UnliftIO run) -> UnliftIO (run . unAuthM)) askUnliftIO)
  withRunInIO go = AuthM (withRunInIO (\k -> go (k . unAuthM)))

There is nothing magical about this; here's how you can derive the definition of askUnliftIO. We want to wrap the existing instance of MonadUnliftIO for ReaderT Auth m. Using that instance, we have:

askUnliftIO :: ReaderT Auth m (UnliftIO (ReaderT Auth m))

And we are looking for

_ :: AuthM m (UnliftIO (AuthM m))

In other words, we want to replace the two occurrences of ReaderT Auth with AuthM. The outer one is easy:

AuthM askUnliftIO :: AuthM m (UnliftIO (ReaderT Auth m))

To get at the inner one, we can use fmap, and then the problem becomes to find the right function UnliftIO (ReaderT Auth m) -> UnliftIO (AuthM m).

fmap _ (AuthM askUnliftIO) :: AuthM m (UnliftIO (AuthM m))

-- provided --

_ :: UnliftIO (ReaderT Auth m) -> UnliftIO (AuthM m)

We're now looking for a function, and the library doesn't provide any functions on UnliftIO, so the only way to start is a lambda with pattern-matching, and since the function result is UnliftIO, we can also start with a constructor:

(\(UnliftIO run) -> UnliftIO (_ :: forall a. AuthM m a -> IO a) :: UnliftIO (AuthM m))
  :: UnliftIO (ReaderT Auth m) -> UnliftIO (AuthM m)

-- where (run :: forall a. ReaderT Auth m a -> IO a)

Here we see that run and the hole only differ in their arguments. We can transform a function's argument by function composition, we fill the hole with run . _, containing a new hole:

(\(UnliftIO run) -> UnliftIO (run . (_ :: AuthM m a -> ReaderT Auth m a)
                                :: forall a. AuthM m a -> IO a
                             )
) :: UnliftIO (ReaderT Auth m) -> UnliftIO (AuthM m)

That hole is finally filled with the destructor \(AuthM u) -> u, aka. unAuthM. Put all the pieces together:

fmap (\(UnliftIO run) -> UnliftIO (run . unAuthM)) (AuthM askUnliftIO)

Note that fmap f (AuthM u) = AuthM (fmap f u) (by definition of fmap for AuthM), which is how you get the version at the top. Whether or not to do that bit of rewriting is mostly a matter of taste.

Most of these steps can be carried out with the help of GHC's typed holes. There's some loose ends at the beginning when you try to find the right shape for the expression, but there might also be a way to use typed holes to help with that part of the exploration as well.

Note that none of this requires any knowledge about the purpose of askUnliftIO nor AuthM. It's 100% mindless wrapping/unwrapping between AuthM and ReaderT, i.e., 100% boilerplate that could be automated, which is the topic of this next section.

Derive

Technical explanation of why deriving doesn't Just Work. The extension GeneralizedNewtypeDeriving tries to coerce ReaderT Auth m (UnliftIO (ReaderT Auth m)) to AuthM m (UnliftIO (AuthM m)) (in the case of askUnliftIO). However, this is not possible if m depends on its argument nominally.

We need a "representational role" constraint, which we can encode as follows thanks to QuantifiedConstraints which appeared in GHC 8.6.

{-# LANGUAGE QuantifiedConstraints, RankNTypes, KindSignatures #-}
-- Note: GHC >= 8.6

import Data.Coerce
import Data.Kind (Constraint)

type Representational m
  = (forall a b. Coercible a b => Coercible (m a) (m b) :: Constraint)
  -- ^ QuantifiedConstraints + RankNTypes               ^ KindSignatures

Thus annotate the derived instance with that constraint:

{-# LANGUAGE GeneralizedNewtypeDeriving, StandaloneDeriving #-}

deriving instance (MonadUnliftIO m, Representational m) => MonadUnliftIO (AuthM m)

Full snippet:

{-# LANGUAGE GeneralizedNewtypeDeriving, StandaloneDeriving, QuantifiedConstraints, KindSignatures, RankNTypes #-}

module Try13 where

import Control.Monad.Reader
import UnliftIO
import Control.Monad.Catch
import Data.Coerce
import Data.Kind (Constraint)

data Auth = Auth

newtype AuthM m a = AuthM { unAuthM :: ReaderT Auth m a }
  deriving(Functor, Applicative, Monad, MonadReader Auth, MonadIO, MonadThrow, MonadCatch, MonadMask)

type Representational m = (forall a b. Coercible a b => Coercible (m a) (m b) :: Constraint)

deriving instance (MonadUnliftIO m, Representational m) => MonadUnliftIO (AuthM m)

-- instance MonadUnliftIO m => MonadUnliftIO (AuthM m) where
--   askUnliftIO = AuthM (fmap (\(UnliftIO run) -> UnliftIO (run . unAuthM)) askUnliftIO)
--   withRunInIO go = AuthM (withRunInIO (\k -> go (k . unAuthM)))
Guidebook answered 25/7, 2019 at 12:28 Comment(3)
Wow! Thank you for this insightful answer. The second part is some new level of type-fu that I haven't seen till now. Let me play around with this with my original and come back with the results!Corr
In which GHC version was QuantifiedConstraints introduced? i'm getting an Unsupported extension error.Corr
It appeared in GHC 8.6 downloads.haskell.org/~ghc/latest/docs/html/users_guide/…Guidebook
T
4

As of version 0.2.0.0 of unliftio-core, the askUnliftIO function has been moved out of the typeclass, which makes it possible to newtype-derive this instance again!

data FeatureFlag
data Auth auth

newtype AuthM (fs :: [FeatureFlag]) auth m a = AuthM
  { unAuthM :: Auth auth -> m a
  }
  deriving newtype
    ( Functor
    , Applicative
    , Monad
    , MonadReader (Auth auth)
    , MonadIO
    , MonadThrow
    , MonadCatch
    , MonadMask
    , MonadUnliftIO
    )

cf https://github.com/fpco/unliftio/issues/55

Trepang answered 15/6, 2020 at 10:5 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.