组合/混合 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.Time
的 T.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 转换器,你可能更愿意合并你的 TimePure
和 TimeEff
。 newtype TimeT m a = TimeT (ReaderT UTCTime m a)
将允许您将选定的 UTCTime
注入到不包含 IO
的效果堆栈中(或者其约束不确保 IO)。然后你可以根据 TimeT
定义 TimePure
,因为 transformers
定义 Reader
和其余的。
您的问题是TimeEff
,只是不需要。接口分离是 classes 类型,而不是具体的 Monad。 TimePure
很好,因为您需要一些 Monad 来提供测试工具,但由于任何旧的 MonadIO
都可以解决 IO 问题,您只是不需要为此指定一个具体的 Monad。
TimeEff
因为它只给你的程序增加了一件事,那就是需要使用 lift
将 TimeEff 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
已经有 Applicative
和 Functor
作为超级classes。所以那些都是免费的。现在就个人品味而言,我什至会忽略 Monad
class GetsTime m where
getCurrentTime :: m UTCTime
这种解耦很好,既因为它使您的代码更通用,也因为它消除了与代数的任何关系。这里的 class 真的没有规律,只是不是代数的,所以恕我直言,让这些关系保持开放是件好事。这意味着您需要在某些地方添加注释,但我觉得将代数约束和有效约束分开记录是一件好事。
getCurrentDay :: (Functor m, TimeMonad m) => m Day
getCurrentDay = T.utctDay <$> getCurrentTime
我目前正在重构一些与 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.Time
的 T.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 转换器,你可能更愿意合并你的 TimePure
和 TimeEff
。 newtype TimeT m a = TimeT (ReaderT UTCTime m a)
将允许您将选定的 UTCTime
注入到不包含 IO
的效果堆栈中(或者其约束不确保 IO)。然后你可以根据 TimeT
定义 TimePure
,因为 transformers
定义 Reader
和其余的。
您的问题是TimeEff
,只是不需要。接口分离是 classes 类型,而不是具体的 Monad。 TimePure
很好,因为您需要一些 Monad 来提供测试工具,但由于任何旧的 MonadIO
都可以解决 IO 问题,您只是不需要为此指定一个具体的 Monad。
TimeEff
因为它只给你的程序增加了一件事,那就是需要使用 lift
将 TimeEff 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
已经有 Applicative
和 Functor
作为超级classes。所以那些都是免费的。现在就个人品味而言,我什至会忽略 Monad
class GetsTime m where
getCurrentTime :: m UTCTime
这种解耦很好,既因为它使您的代码更通用,也因为它消除了与代数的任何关系。这里的 class 真的没有规律,只是不是代数的,所以恕我直言,让这些关系保持开放是件好事。这意味着您需要在某些地方添加注释,但我觉得将代数约束和有效约束分开记录是一件好事。
getCurrentDay :: (Functor m, TimeMonad m) => m Day
getCurrentDay = T.utctDay <$> getCurrentTime