为所有MonadTrans实例提供类型类实例

时间:2015-02-15 17:13:20

标签: haskell typeclass monad-transformers

我已经定义了我自己的monad变换器:

data Config = Config { ... }

data State = State { ... }

newtype FooT m a = FooT {
      runFoo :: ReaderT Config (StateT State m) a
    } deriving (Functor, Monad, MonadReader Config, MonadState State)

我已经为它定义了MonadTrans个实例。

instance MonadTrans FooT where
   lift = FooT . lift . lift

现在,我有各种各样的monad,我只能由编译器为我派生。我以MonadIO为例。所以我已将MonadIO实例定义为

instance MonadIO m => MonadIO (FooT m) where
    liftIO = lift . liftIO

然而,我发现我为每个Monad做了很多提升。为什么每个Monad类型类的作者(即MonadIO,MonadCatchIO,MonadFoo)都没有根据MonadTrans定义一个通用实例,而不是让我为每个新的MonadTrans实现一个实例? a la

instance (MonadIO m, MonadTrans t, Monad (t m)) => MonadIO (t m) where
  liftIO = lift . liftIO

这需要UndecidableInstances进行编译,而且我不确定它是否正确(事实上,非常确定它是不正确的),但是现在用来表达我的意图

那么,这可能吗?如果没有,为什么不呢?会不会是?

1 个答案:

答案 0 :(得分:-1)

让我们说我已经找到了MonadIO的替代品,叫做 MyMonadIO。除了名称之外,它在各个方面都像MonadIO

class Monad m => MyMonadIO m where
  myLiftIO :: IO a -> m a

假设您的FooT类型:

newtype FooT m a = FooT
  { runFoo :: ReaderT Config (StateT AppState m) a
  } deriving (Functor, Applicative, Monad, MonadReader Config, MonadState AppState)

可以为ReaderT创建MyMonadIO的实例, StateT,最后是FooT。我添加了额外的类型注释来实现它 读者更容易弄清楚发生了什么:

instance MyMonadIO m => MyMonadIO (ReaderT r m) where
  myLiftIO :: IO a -> ReaderT r m a
  myLiftIO = (lift :: m a -> ReaderT r m a) . (myLiftIO :: IO a -> m a)

instance MyMonadIO m => MyMonadIO (StateT s m) where
  myLiftIO :: IO a -> StateT s m a
  myLiftIO = (lift :: m a -> StateT s m a) . (myLiftIO :: IO a -> m a)

instance MyMonadIO m => MyMonadIO (FooT m) where
  myLiftIO :: IO a -> FooT m a
  myLiftIO = (lift :: m a -> FooT m a) . (myLiftIO :: IO a -> m a)

也可以使用GeneralizedNewtypeDeriving轻松推导出来 MyMonadIO FooT(假设已有ReaderTStateT的实例 newtype FooT m a = FooT { runFoo :: ReaderT Config (StateT AppState m) a } deriving (Functor, Applicative, Monad, MyMonadIO, MonadReader Config, MonadState AppState) ):

myLiftIO

如果您查看ReaderTStateT的{​​{1}}函数的正文, 和FooT个实例,它们完全相同:lift . myLiftIO

以下是问题的重复:

  

为什么每个Monad类型类的作者(即MonadIO,MonadCatchIO,   MonadFoo)没有用MonadTrans来定义一个通用实例,而不是   让我为每个新的MonadTrans实现一个实例我想出来了吗?

对于MyMonadIO,这个一般情况如下:

instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n) where
  myLiftIO :: IO a -> t n a
  myLiftIO = (lift :: n a -> t n a) . (myLiftIO :: IO a -> n a)

定义此实例后,您不需要ReaderT的特定实例, StateT,甚至是FooT

这需要UndecidableInstances。但是,这个问题不是不可判定性,而是这个实例与MyMonadIO的一些可能有效的实例重叠。

例如,想象一下以下数据类型:

newtype FreeIO f a = FreeIO (IO (Either a (f (FreeIO f a))))

instance Functor f => Functor (FreeIO f) where
  fmap :: (a -> b) -> FreeIO f a -> FreeIO f b
  fmap f (FreeIO io) = FreeIO $ do
    eitherA <- io
    pure $
      case eitherA of
        Left a -> Left $ f a
        Right fFreeIO -> Right $ fmap f <$> fFreeIO

instance Functor f => Applicative (FreeIO f) where
  pure :: a -> FreeIO f a
  pure a = FreeIO . pure $ Left a

  (<*>) :: FreeIO f (a -> b) -> FreeIO f a -> FreeIO f b
  (<*>) (FreeIO ioA2b) (FreeIO ioA) = FreeIO $ do
    eitherFa2b <- ioA2b
    eitherFa <- ioA
    pure $
      case (eitherFa2b, eitherFa) of
        (Left a2b, Left a) -> Left $ a2b a
        (Left a2b, Right fFreeIOa) -> Right $ fmap a2b <$> fFreeIOa
        (Right fFreeIOa2b, o) -> Right $ (<*> FreeIO (pure o)) <$> fFreeIOa2b

instance Functor f => Monad (FreeIO f) where
  (>>=) :: FreeIO f a -> (a -> FreeIO f b) -> FreeIO f b
  (>>=) (FreeIO ioA) mA2b = FreeIO $ do
    eitherFa <- ioA
    case eitherFa of
      Left a ->
        let (FreeIO ioB) = mA2b a
        in ioB
      Right fFreeIOa -> pure . Right $ fmap (>>= mA2b) fFreeIOa

您不一定需要了解此FreeIO数据类型(尤其是FunctorApplicativeMonad实例。只知道这是一种有效的数据类型就足够了。

(如果您有兴趣,这只是一个free monad缠绕IO。)

可以为MyMonadIO编写FreeIO个实例:

instance Functor f => MyMonadIO (FreeIO f) where
  myLiftIO :: IO a -> FreeIO f a
  myLiftIO ioA = FreeIO (Left <$> ioA)

我们甚至可以想象使用FreeIO编写函数:

tryMyLiftIOWithFreeIO :: Functor f => FreeIO f ()
tryMyLiftIOWithFreeIO = myLiftIO $ print "hello"

如果您尝试使用此实例(tryMyLiftIOWithFreeIO)和上面的错误实例编译MyMonadIO (FreeIO f),则会收到以下错误:

test-monad-trans.hs:103:25: error:
    • Overlapping instances for MyMonadIO (FreeIO f)
        arising from a use of ‘myLiftIO’
      Matching instances:
        instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n)
          -- Defined at test-monad-trans.hs:52:10
        instance Functor f => MyMonadIO (FreeIO f)
          -- Defined at test-monad-trans.hs:98:10
    • In the expression: myLiftIO $ print "hello"
      In an equation for ‘tryMyLiftIOWithFreeIO’:
          tryMyLiftIOWithFreeIO = myLiftIO $ print "hello"

为什么会这样?

那么,在instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n)中,tn的类型是什么?

由于n应该是Monad,所以它的种类是* -> *。由于t是一个monad变换器,它的类型是(* -> *) -> * -> *t n也应该是Monad,所以它的种类也是* -> *

n :: * -> *
t :: (* -> *) -> * -> *
t n :: * -> *

现在,在instance Functor f => MyMonadIO (FreeIO f)中,FreeIOf的种类是什么?

f应该是Functor,所以它的种类是* -> *FreeIO的种类是(* -> *) -> * -> *FreeIO fMonad,所以它是* -> *

f :: * -> *
FreeIO :: (* -> *) -> * -> *
FreeIO f :: * -> *

由于种类相同,因此您可以看到instance Functor f => MyMonadIO (FreeIO f)instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n)重叠。 GHC不确定选哪一个!

您可以将实例FreeIO实例标记为OVERLAPPING

来解决此问题
instance {-# OVERLAPPING #-} Functor f => MyMonadIO (FreeIO f) where
  myLiftIO :: IO a -> FreeIO f a
  myLiftIO m = FreeIO (Left <$> m)

然而,这是一条堕落的危险路线。您可以在GHC user guide找到更多关于重叠可能不好的原因。

FreeIO示例由Edward Kmett创建。您可以在this reddit post中找到另一个重叠实例的聪明示例。

如果您打算编写monad类型类(如MyMonadIO)和 将它发布到Hackage,一种选择是使用 DefaultSignatures 功能。这使您的库用户更容易定义 实例

使用DefaultSignatures,定义MyMonadIO类将如下所示:

class Monad m => MyMonadIO m where
  myLiftIO :: IO a -> m a
  default myLiftIO
    :: forall t n a.
       ( MyMonadIO n
       , MonadTrans t
       , m ~ t n
       )
    => IO a -> t n a
  myLiftIO = (lift :: n a -> t n a) . (myLiftIO :: IO a -> n a)

这表示任何myLiftIO都有t n的默认实施, 其中nMyMonadIO的实例,而t是其实例 MonadTrans

使用此myLiftIO的默认模式,为MyMonadIOReaderT定义StateT的实例将如下所示:

instance MyMonadIO m => MyMonadIO (ReaderT r m)
instance MyMonadIO m => MyMonadIO (StateT s m)

很简单。从那以后,您不需要提供myLiftIO的函数体 它将使用默认值。

唯一的缺点是它没有广泛完成。该 DefaultSignatures机制似乎主要用于generic programming,而不是monad类型。