Blu*_*ers 7 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)
Run Code Online (Sandbox Code Playgroud)
我已经MonadTrans为它定义了一个实例。
instance MonadTrans FooT where
lift = FooT . lift . lift
Run Code Online (Sandbox Code Playgroud)
现在,我有各种单子,我不能仅仅由编译器为我派生这些单子。我就拿MonadIO这个例子来说吧。所以我将我的MonadIO实例定义为
instance MonadIO m => MonadIO (FooT m) where
liftIO = lift . liftIO
Run Code Online (Sandbox Code Playgroud)
然而,我发现我为每个 Monad 做了很多提升。为什么每个 Monad 类型类(即 MonadIO、MonadCatchIO、MonadFoo)的作者不能根据 MonadTrans 定义一个通用实例,而不是让我为每个新的 MonadTrans 实现一个实例?阿拉
instance (MonadIO m, MonadTrans t, Monad (t m)) => MonadIO (t m) where
liftIO = lift . liftIO
Run Code Online (Sandbox Code Playgroud)
这需要UndecidableInstances编译,我不确定它是否正确(事实上,很确定它是不正确的),但目前可以表达我的意图。
那么,这可能吗?如果没有,为什么不呢?会是这样吗?
假设我已经想出了一个替代方案MonadIO,称为\n MyMonadIO。MonadIO除了名字之外,其他方面都很像:
class Monad m => MyMonadIO m where\n myLiftIO :: IO a -> m a\nRun Code Online (Sandbox Code Playgroud)\n\n假设您的FooT类型:
newtype FooT m a = FooT\n { runFoo :: ReaderT Config (StateT AppState m) a\n } deriving (Functor, Applicative, Monad, MonadReader Config, MonadState AppState)\nRun Code Online (Sandbox Code Playgroud)\n\n可以创建MyMonadIOfor ReaderT、\nStateT和 finally的实例FooT。我添加了额外的类型注释,以使读者更容易弄清楚发生了什么:
instance MyMonadIO m => MyMonadIO (ReaderT r m) where\n myLiftIO :: IO a -> ReaderT r m a\n myLiftIO = (lift :: m a -> ReaderT r m a) . (myLiftIO :: IO a -> m a)\n\ninstance MyMonadIO m => MyMonadIO (StateT s m) where\n myLiftIO :: IO a -> StateT s m a\n myLiftIO = (lift :: m a -> StateT s m a) . (myLiftIO :: IO a -> m a)\n\ninstance MyMonadIO m => MyMonadIO (FooT m) where\n myLiftIO :: IO a -> FooT m a\n myLiftIO = (lift :: m a -> FooT m a) . (myLiftIO :: IO a -> m a)\nRun Code Online (Sandbox Code Playgroud)\n\n也可以用来GeneralizedNewtypeDeriving轻松导出 for MyMonadIO(FooT假设已经有ReaderTand的实例StateT):
newtype FooT m a = FooT\n { runFoo :: ReaderT Config (StateT AppState m) a\n } deriving (Functor, Applicative, Monad, MyMonadIO, MonadReader Config, MonadState AppState)\nRun Code Online (Sandbox Code Playgroud)\n\n如果您查看, ,\nand实例的myLiftIO函数体,它们是完全相同的:。ReaderTStateTFooTlift . myLiftIO
这是问题的重复:
\n\n\n\n\n为什么每个 Monad 类型类(即 MonadIO、MonadCatchIO、MonadFoo)的作者不能根据 MonadTrans 定义一个通用实例,而不是让我为每个新的 MonadTrans 实现一个实例?
\n
对于MyMonadIO,这个一般实例如下:
instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n) where\n myLiftIO :: IO a -> t n a\n myLiftIO = (lift :: n a -> t n a) . (myLiftIO :: IO a -> n a)\nRun Code Online (Sandbox Code Playgroud)\n\n定义此实例后,您不需要ReaderT、\nStateT甚至 的特定实例FooT。
这需要UndecidableInstances. 然而,问题不在于不可判定性,而在于该实例与 的一些潜在有效实例重叠MyMonadIO。
例如,想象以下数据类型:
\n\nnewtype FreeIO f a = FreeIO (IO (Either a (f (FreeIO f a))))\n\ninstance Functor f => Functor (FreeIO f) where\n fmap :: (a -> b) -> FreeIO f a -> FreeIO f b\n fmap f (FreeIO io) = FreeIO $ do\n eitherA <- io\n pure $\n case eitherA of\n Left a -> Left $ f a\n Right fFreeIO -> Right $ fmap f <$> fFreeIO\n\ninstance Functor f => Applicative (FreeIO f) where\n pure :: a -> FreeIO f a\n pure a = FreeIO . pure $ Left a\n\n (<*>) :: FreeIO f (a -> b) -> FreeIO f a -> FreeIO f b\n (<*>) (FreeIO ioA2b) (FreeIO ioA) = FreeIO $ do\n eitherFa2b <- ioA2b\n eitherFa <- ioA\n pure $\n case (eitherFa2b, eitherFa) of\n (Left a2b, Left a) -> Left $ a2b a\n (Left a2b, Right fFreeIOa) -> Right $ fmap a2b <$> fFreeIOa\n (Right fFreeIOa2b, o) -> Right $ (<*> FreeIO (pure o)) <$> fFreeIOa2b\n\ninstance Functor f => Monad (FreeIO f) where\n (>>=) :: FreeIO f a -> (a -> FreeIO f b) -> FreeIO f b\n (>>=) (FreeIO ioA) mA2b = FreeIO $ do\n eitherFa <- ioA\n case eitherFa of\n Left a ->\n let (FreeIO ioB) = mA2b a\n in ioB\n Right fFreeIOa -> pure . Right $ fmap (>>= mA2b) fFreeIOa\nRun Code Online (Sandbox Code Playgroud)\n\n您不一定需要了解此FreeIO数据类型(尤其是Functor、Applicative和Monad实例)。只需知道这是一个有效的数据类型就足够了。
(如果您感兴趣,这只是一个包裹在 . 中的免费 monadIO。)
可以编写一个MyMonadIO实例FreeIO:
instance Functor f => MyMonadIO (FreeIO f) where\n myLiftIO :: IO a -> FreeIO f a\n myLiftIO ioA = FreeIO (Left <$> ioA)\nRun Code Online (Sandbox Code Playgroud)\n\n我们甚至可以想象使用以下方法编写一个函数FreeIO:
tryMyLiftIOWithFreeIO :: Functor f => FreeIO f ()\ntryMyLiftIOWithFreeIO = myLiftIO $ print "hello"\nRun Code Online (Sandbox Code Playgroud)\n\ntryMyLiftIOWithFreeIO如果您尝试使用此实例 ( ) 和上面的错误实例进行编译MyMonadIO (FreeIO f),您会收到以下错误:
test-monad-trans.hs:103:25: error:\n \xe2\x80\xa2 Overlapping instances for MyMonadIO (FreeIO f)\n arising from a use of \xe2\x80\x98myLiftIO\xe2\x80\x99\n Matching instances:\n instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n)\n -- Defined at test-monad-trans.hs:52:10\n instance Functor f => MyMonadIO (FreeIO f)\n -- Defined at test-monad-trans.hs:98:10\n \xe2\x80\xa2 In the expression: myLiftIO $ print "hello"\n In an equation for \xe2\x80\x98tryMyLiftIOWithFreeIO\xe2\x80\x99:\n tryMyLiftIOWithFreeIO = myLiftIO $ print "hello"\nRun Code Online (Sandbox Code Playgroud)\n\n为什么会出现这种情况?
\n\n那么,在 中, andinstance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n)是什么类型的?tn
由于n应该是 a Monad,所以它的种类是* -> *。由于t是 monad 转换器,因此它的种类是(* -> *) -> * -> *。 t n也应该是 a Monad,所以它的种类也是* -> *:
n :: * -> *\nt :: (* -> *) -> * -> *\nt n :: * -> *\nRun Code Online (Sandbox Code Playgroud)\n\n现在,在 中,和 的instance Functor f => MyMonadIO (FreeIO f)种类是什么?FreeIOf
f应该是 a Functor,所以它的种类是* -> *。 FreeIO\ 的种类是(* -> *) -> * -> *. FreeIO f是 a Monad,所以它的种类是* -> *:
f :: * -> *\nFreeIO :: (* -> *) -> * -> *\nFreeIO f :: * -> *\nRun Code Online (Sandbox Code Playgroud)\n\n由于种类相同,您会看到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\n myLiftIO :: IO a -> FreeIO f a\n myLiftIO m = FreeIO (Left <$> m)\nRun Code Online (Sandbox Code Playgroud)\n\n然而,这是一条危险的下降之路。您可以从GHC 用户指南中了解有关重叠为何不好的更多信息。
\n\n此FreeIO示例由 Edward Kmett 创建。您可以在这篇 reddit 帖子中找到另一个巧妙的重叠实例示例。
如果您计划编写一个 monad 类型类(如MyMonadIO)并将其发布到 Hackage,一种选择是使用该功能DefaultSignatures。这使得您的库的用户可以更轻松地定义\实例。
使用DefaultSignatures,定义MyMonadIO类将如下所示:
class Monad m => MyMonadIO m where\n myLiftIO :: IO a -> m a\n default myLiftIO\n :: forall t n a.\n ( MyMonadIO n\n , MonadTrans t\n , m ~ t n\n )\n => IO a -> t n a\n myLiftIO = (lift :: n a -> t n a) . (myLiftIO :: IO a -> n a)\nRun Code Online (Sandbox Code Playgroud)\n\nmyLiftIO这表示对于任何都有一个默认实现t n,\n其中n是 的一个实例MyMonadIO,并且t是\n 的一个实例MonadTrans。
使用 for 的默认符号myLiftIO,定义MyMonadIOforReaderT和的实例StateT将如下所示:
instance MyMonadIO m => MyMonadIO (ReaderT r m)\ninstance MyMonadIO m => MyMonadIO (StateT s m)\nRun Code Online (Sandbox Code Playgroud)\n\n很简单。您不需要提供函数体,myLiftIO因为\nit 将使用默认值。
这样做的唯一缺点是它没有被广泛实施。该DefaultSignatures机器似乎主要用于通用编程,而不是 monad 类型类。