Haskell 组合/混合mtl样式类型类约束时的隐式提升
我目前正在重构一些Haskell代码,这些代码与Haskell 组合/混合mtl样式类型类约束时的隐式提升,haskell,monad-transformers,Haskell,Monad Transformers,我目前正在重构一些Haskell代码,这些代码与Data.Time交互。最终,我有一系列与时间交互的函数: getCurrentTime :: IO UTCTime getCurrentTime = T.getCurrentTime getCurrentDay :: IO Day getCurrentDay = T.utctDay <$> getCurrentTime daysUntil :: Day -> IO Integer daysUntil day = T.diff
Data.Time
交互。最终,我有一系列与时间交互的函数:
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
,其余的实现也都是如此
尽管最近我一直在阅读Haskell中关于存根和伪造效果的文章,并且希望能够为getCurrentTime
运行带有伪造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
我必须这样调整我的功能:
markArticleRead :: (MonadIO m, TimeMonad m) => Key Article -> SqlPersistT m ()
markArticleRead articleKey =
updateLastModified articleKey =<< lift getCurrentTime
markArticleRead::(MonadIO m,TimeMonad m)=>Key Article->sqlpersist m()
markArticleRead articleKey=
updateLastModified articleKey=对于mtl样式的效果,通常为常见的monad转换器定义提升实例。例如TimeMonad m=>TimeMonad(ReaderT r m)
。这样,您就可以在markArticleRead
中省去lift
另一个选项是跳过monad transformerTimeEff
。它不携带任何附加信息,您也不需要提及需要防止在其他MonadIO
类型中调用时间函数。如果编写实例MonadIO m=>TimeMonad m
,则markArticleRead
不需要TimeMonad
约束或lift
。此实例与第一段中的实例重叠;挑一个
如果您确实想要monad转换器,您可能更愿意合并TimePure
和TimeEff
newtype TimeT m a=TimeT(ReaderT UTCTime m a)
允许您将所选的UTCTime
注入不包含IO
(或其约束不能确保IO)的有效堆栈中。然后您可以根据TimeT
定义TimePure
,就像transformers
定义Reader
和其他一样。您的问题是TimeEff
,它只是不需要。接口分离是类型类,而不是具体的monadTimePure
很好,因为您需要一些Monad来提供测试工具,但是由于任何旧的MonadIO
都可以为IO案例提供技巧,因此您不需要为此指定具体的Monad
TimeEff
在您的程序中只添加了一项功能,这就是需要使用lift
将TimeEff m
转换为m
。由于这适用于所有的MonadIO
,我们可以使用不可判定的实例
来允许统一,而无需在有效案例中添加TimeMonad
。(我知道不可判定的实例听起来很糟糕,但事实并非如此)
可能是
class Monad m=>TimeMonad m其中
因为Monad
已经有Applicative
和Functor
作为超类。所以这些都是免费的。现在,就个人品味而言,我甚至会省去Monad
class GetsTime m其中
getCurrentTime::m UTCTime
这种解耦很好,既因为它使代码更通用,也因为它消除了与代数的任何关系。这里的类实际上没有定律,只是不是代数的,所以让这些关系保持开放状态是很好的。这意味着您需要在某些地方添加注释,但我觉得将代数约束和有效约束分开记录是一件好事
getCurrentDay::(函子m,TimeMonad m)=>m天
getCurrentDay=T.utctDay getCurrentTime
我不知道您的用例是什么,但我认为将您的业务逻辑放在以当前时间为参数的简单纯函数中,或者放在状态下,更为合理。这里的方式似乎有点过度设计。谢谢你的回答,你介意添加一个代码示例来说明你的意思吗?我尝试按照您的建议使用newtype
TimeT
进行重构,但我仍然遇到了一个问题,在混合DB效果和IO/时间效果时,我需要回到显式lift
ing。。。很有可能我没有按照您尝试的方式实现它,我会在有更多时间时添加代码。简而言之:markArticleRead
根本不使用转换器。如果你遵循我第三段中的建议,你也会希望第一段中有一个实例。我怀疑至少有一条法则,它依赖于一个Applicative
约束:liftA2(getCurrentTime)=liftA2(\\\\\\\\\->True)getCurrentTime(m*>getCurrentTime)
。我想你也会说,getCurrentTime
不会改变世界:类似于()
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
instance (Monad m, MonadIO m) => TimeMonad m where
getCurrentTime = liftIO T.getCurrentTime
markArticleRead :: MonadIO m => Key Article -> SqlPersistT m ()
markArticleRead articleKey =
updateLastModified articleKey =<< getCurrentTime