В настоящее время я выполняю рефакторинг некоторого моего кода на 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
из 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, или это сейчас невозможно из-за введенных мною типов?