组合/混合 mtl 样式类型类约束时的隐式提升

Implicit lifting when combining / mixing mtl-style typeclass constraints

我目前正在重构一些与 Data.Time 交互的 Haskell 代码。最终我有一堆与时间交互的功能:

getCurrentTime :: IO UTCTime
getCurrentTime = T.getCurrentTime

getCurrentDay :: IO Day
getCurrentDay = T.utctDay <$> getCurrentTime

daysUntil :: Day -> IO Integer
daysUntil day = T.diffDays day <$> getCurrentDay

等等等等,最终这些只是我自己的辅助函数,它们都是基于 Data.TimeT.getCurrentTime。这是所有这些功能的'effect'。

我对这段代码所做的第一个重构是简单地将它们更改为使用 MonadIO 以允许它们在与此类型类兼容的各种转换器堆栈中使用:

getCurrentTime :: MonadIO m => m UTCTime
getCurrentTime = liftIO T.getCurrentTime

getCurrentDay :: MonadIO m => m Day
getCurrentDay = T.utctDay <$> getCurrentTime

daysUntil :: MonadIO m => Day -> m Integer
daysUntil day = T.diffDays day <$> getCurrentDay

这很简单,因为我只需要提升 T.getCurrentTime 其余的实现就可以了。

虽然我最近一直在阅读 Haskell 中的存根和伪造效果,但我希望能够 运行 这些函数使用 [=21] 的伪造 UTCTime 结果=].

根据我在网上阅读的一些内容,看看 Pandoc 如何实现分离纯粹和有效的操作,我想出了这个:

newtype TimePure a = TimePure
  { unTimePure :: Reader UTCTime a
  } deriving (Functor, Applicative, Monad, MonadReader UTCTime)

newtype TimeEff m a = TimeEff
  { unTimeIO :: m a
  } deriving (Functor, Applicative, Monad, MonadIO)

class (Functor m, Applicative m, Monad m) => TimeMonad m where
  getCurrentTime :: m UTCTime

instance TimeMonad TimePure where
  getCurrentTime = ask

instance MonadIO m => TimeMonad (TimeEff m) where
  getCurrentTime = liftIO T.getCurrentTime

getCurrentDay :: TimeMonad m => m Day
getCurrentDay = T.utctDay <$> getCurrentTime

daysUntil :: TimeMonad m => Day -> m Integer
daysUntil day = T.diffDays day <$> getCurrentDay

同样,除了顶部的附加定义外,我不需要做太多更改 - 我的原始函数只需要更改为使用 TimeMonad m 而不是 MonadIO m

这是理想的,我现在可以 运行 我的时间在纯粹的环境中发挥作用。

但是现在当我谈到一些真实世界的代码时,给出一个与数据库交互的示例函数:

markArticleRead :: MonadIO m => Key Article -> SqlPersistT m ()
markArticleRead articleKey =
  updateLastModified articleKey =<< getCurrentTime

我必须像这样调整我的功能:

markArticleRead :: (MonadIO m, TimeMonad m) => Key Article -> SqlPersistT m ()
markArticleRead articleKey =
  updateLastModified articleKey =<< lift getCurrentTime

显然我必须这样做,因为 getCurrentTime 不需要 MonadIO 到 运行。我遇到的问题是重新引入升力,这是必需的,因为变压器堆栈有两个 'layers',而不是一个(我认为这是一个合适的解释?)。

引入 MonadIO 的好处之一是它消除了必须到处举东西的需要,并且它制作了这样的功能,其中很多时候包含业务逻辑等,a噪音小了很多。有没有办法让我重新获得这个好处,在那里我可以获得mtl风格的隐式提升,或者由于我引入的类型现在不可能了?

对于 mtl 风格的效果,通常为常见的 monad 转换器定义提升实例。比如TimeMonad m => TimeMonad (ReaderT r m)。这样您就可以在 markArticleRead.

中省略 lift

另一种选择是跳过 monad 转换器 TimeEff。它不包含任何附加信息,您也没有提到需要防止在其他 MonadIO 类型中调用时间函数。如果您编写实例 MonadIO m => TimeMonad m,则 markArticleRead 不需要 TimeMonad 约束或 lift。此实例与第一段中的实例重叠;选一个。

如果你确实想要一个 monad 转换器,你可能更愿意合并你的 TimePureTimeEffnewtype TimeT m a = TimeT (ReaderT UTCTime m a) 将允许您将选定的 UTCTime 注入到不包含 IO 的效果堆栈中(或者其约束不确保 IO)。然后你可以根据 TimeT 定义 TimePure,因为 transformers 定义 Reader 和其余的。

您的问题是TimeEff,只是不需要。接口分离是 classes 类型,而不是具体的 Monad。 TimePure 很好,因为您需要一些 Monad 来提供测试工具,但由于任何旧的 MonadIO 都可以解决 IO 问题,您只是不需要为此指定一个具体的 Monad。

TimeEff 因为它只给你的程序增加了一件事,那就是需要使用 liftTimeEff m 转换为 m。由于这对所有 MonadIO 都有效,我们可以使用 UndecidableInstances 来允许统一,甚至无需将 TimeMonad 添加到有效情况。 (我知道 UndecidableInstances 听起来很糟糕,但事实并非如此)

Running example

instance (Monad m, MonadIO m) => TimeMonad m where
  getCurrentTime = liftIO T.getCurrentTime

markArticleRead :: MonadIO m => Key Article -> SqlPersistT m ()
markArticleRead articleKey =
  updateLastModified articleKey =<< getCurrentTime

一些其他笔记。

class (Functor m, Applicative m, Monad m) => TimeMonad m where

可以

class Monad m => TimeMonad m where

因为 Monad 已经有 ApplicativeFunctor 作为超级classes。所以那些都是免费的。现在就个人品味而言,我什至会忽略 Monad

class GetsTime m where
  getCurrentTime :: m UTCTime

这种解耦很好,既因为它使您的代码更通用,也因为它消除了与代数的任何关系。这里的 class 真的没有规律,只是不是代数的,所以恕我直言,让这些关系保持开放是件好事。这意味着您需要在某些地方添加注释,但我觉得将代数约束和有效约束分开记录是一件好事。

getCurrentDay :: (Functor m, TimeMonad m) => m Day
getCurrentDay = T.utctDay <$> getCurrentTime