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

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

我目前正在重构一些Haskell代码,这些代码与
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 transformer
TimeEff
。它不携带任何附加信息,您也不需要提及需要防止在其他
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
,它只是不需要。接口分离是类型类,而不是具体的monad
TimePure
很好,因为您需要一些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