我目前正在重构与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样式,还是由于我介绍的类型而现在不可能?
答案 0 :(得分:1)
对于mtl风格的效果,通常为常见的monad变压器定义提升实例。如TimeMonad m => TimeMonad (ReaderT r m)
。这样您就可以省去lift
中的markArticleRead
。
另一个选择是跳过monad变压器TimeEff
。它不包含任何其他信息,您也没有提到需要防止在其他MonadIO
类型中调用时间函数。如果您编写实例MonadIO m => TimeMonad m
,则markArticleRead
不需要TimeMonad
约束或lift
。此实例与第一段中的实例重叠;选一个。
如果您确实希望使用monad变压器,则可能希望合并TimePure
和TimeEff
。 newtype 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
,因为它只会在程序中添加一件事,因此需要使用lift
将TimeEff m
转换为m
。并且由于这对所有MonadIO
都适用,因此我们可以使用UndecidableInstances
来允许统一,而无需在有效案例中添加TimeMonad
。 (我知道UndecidableInstances
听起来很糟糕,但不是)
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
已经具有Applicative
和Functor
作为超类。因此,这些免费提供。现在,根据个人喜好,我什至省去Monad
class GetsTime m where
getCurrentTime :: m UTCTime
这种解耦非常好,不仅因为它使您的代码更通用,而且还因为它消除了与代数的任何关系。这里的班级确实没有法律,只是没有代数关系,因此最好将这些关系保持开放状态。这意味着您需要在某些地方添加注释,但是我觉得分开记录代数约束和有效约束是一件好事。
getCurrentDay :: (Functor m, TimeMonad m) => m Day
getCurrentDay = T.utctDay <$> getCurrentTime