合并/混合MTL样式类型类约束时的隐式提升

时间:2019-02-07 23:02:52

标签: haskell monad-transformers

我目前正在重构与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

等等等,最终,这些只是我自己的帮助器函数,它们全部基于T.getCurrentTime中的Data.Time。这是所有这些功能的“效果”。

我对此代码进行的第一个重构是简单地将它们更改为使用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中的存根和伪造效果的信息,并且希望能够以UTCTime的伪getCurrentTime结果运行这些功能。

结束一些我在网上阅读的内容,看看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。我遇到的问题是重新引入提升,这是需要的,因为变压器堆栈有两个“层”,而不是一层(我认为这是一个适当的解释?)。

引入MonadIO的好处之一是,它消除了必须随处举起东西的麻烦,并且使诸如此类的功能(很多时候包含业务逻辑等)变得更少了。吵。我是否有办法重新获得这种好处,可以隐式提升mtl样式,还是由于我介绍的类型而现在不可能?

2 个答案:

答案 0 :(得分:1)

对于mtl风格的效果,通常为常见的monad变压器定义提升实例。如TimeMonad m => TimeMonad (ReaderT r m)。这样您就可以省去lift中的markArticleRead

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

如果您确实希望使用monad变压器,则可能希望合并TimePureTimeEffnewtype TimeT m a = TimeT (ReaderT UTCTime m a)将允许您将选定的UTCTime注入不包含IO(或约束不能确保IO)的效果堆栈中。然后,您可以用TimePure来定义TimeT,就像transformers定义了Reader一样,

答案 1 :(得分:1)

您的问题是TimeEff,只是不需要。接口分隔是类型类,而不是具体的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作为超类。因此,这些免费提供。现在,根据个人喜好,我什至省去Monad

class GetsTime m where
  getCurrentTime :: m UTCTime

这种解耦非常好,不仅因为它使您的代码更通用,而且还因为它消除了与代数的任何关系。这里的班级确实没有法律,只是没有代数关系,因此最好将这些关系保持开放状态。这意味着您需要在某些地方添加注释,但是我觉得分开记录代数约束和有效约束是一件好事。

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