Объединение нескольких состояний в StateT
Я пишу программу, которая работает как демон.
Для создания демона пользователь предоставляет набор
реализации для каждого из необходимых классов (одна из них - база данных)
Все эти классы имеют функции
типа подписи формы StateT s IO a
,
но s
для каждого класса отличается.
Предположим, что каждый из классов следует этой схеме:
import Control.Monad (liftM)
import Control.Monad.State (StateT(..), get)
class Hammer h where
driveNail :: StateT h IO ()
data ClawHammer = MkClawHammer Int -- the real implementation is more complex
instance Hammer ClawHammer where
driveNail = return () -- the real implementation is more complex
-- Plus additional classes for wrenches, screwdrivers, etc.
Теперь я могу определить запись, представляющую реализацию, выбранную
пользователя для каждого "слота".
data MultiTool h = MultiTool {
hammer :: h
-- Plus additional fields for wrenches, screwdrivers, etc.
}
И демон делает большую часть своей работы в StateT (MultiTool h ...) IO ()
монада.
Теперь, поскольку в мультиуровне есть молоток, я могу использовать его в любой ситуации
где нужен молот. Другими словами, тип MultiTool
может реализовать любой из классов, которые он содержит, если я пишу код следующим образом:
stateMap :: Monad m => (s -> t) -> (t -> s) -> StateT s m a -> StateT t m a
stateMap f g (StateT h) = StateT $ liftM (fmap f) . h . g
withHammer :: StateT h IO () -> StateT (MultiTool h) IO ()
withHammer runProgram = do
t <- get
stateMap (\h -> t {hammer=h}) hammer runProgram
instance Hammer h => Hammer (MultiTool h) where
driveNail = withHammer driveNail
Но реализации withHammer
, withWrench
, withScrewdriver
и т.д.
в основном идентичны. Было бы неплохо написать что-нибудь
как это...
--withMember accessor runProgram = do
-- u <- get
-- stateMap (\h -> u {accessor=h}) accessor runProgram
-- instance Hammer h => Hammer (MultiTool h) where
-- driveNail = withMember hammer driveNail
Но, конечно, это не скомпилируется.
Я подозреваю, что мое решение слишком объектно-ориентированное.
Есть ли способ лучше?
Модадские трансформаторы, может быть?
Заранее благодарю вас за любые предложения.
Ответы
Ответ 1
Если вы хотите пойти с большим глобальным состоянием, как в вашем случае, то то, что вы хотите использовать, - это линзы, как предположил Бен. Я также рекомендую библиотеку Edward Kmett объектива. Однако есть и другой, возможно, более приятный способ.
Серверы обладают тем свойством, что программа работает непрерывно и выполняет ту же операцию над пространством состояний. Проблема начинается, когда вы хотите модулировать свой сервер, и в этом случае вы хотите больше, чем просто какое-то глобальное состояние. Вы хотите, чтобы модули имели собственное состояние.
Подумайте о модуле как о чем-то, что преобразует запрос в ответ:
Module :: (Request -> m Response) -> Module m
Теперь, если у него есть какое-то состояние, это состояние становится заметным, поскольку в следующий раз модуль может дать другой ответ. Существует несколько способов сделать это, например, следующее:
Module :: s -> ((Request, s) -> m (Response s)) -> Module m
Но гораздо более удобный и эквивалентный способ выразить это следующий конструктор (мы скоро построим тип вокруг него):
Module :: (Request -> m (Response, Module m)) -> Module m
Этот модуль отображает запрос на ответ, но по пути также возвращает новую версию. Отпустите дальше и сделайте запросы и ответы полиморфными:
Module :: (a -> m (b, Module m a b)) -> Module m a b
Теперь, если тип вывода модуля соответствует другому типу ввода модуля, вы можете составить их как обычные функции. Эта композиция ассоциативна и имеет полиморфную идентичность. Это очень похоже на категорию, и на самом деле это так! Это категория, аппликативный функтор и стрелка.
newtype Module m a b =
Module (a -> m (b, Module m a b))
instance (Monad m) => Applicative (Module m a)
instance (Monad m) => Arrow (Module m)
instance (Monad m) => Category (Module m)
instance (Monad m) => Functor (Module m a)
Теперь мы можем составить два модуля, которые имеют собственное индивидуальное локальное состояние, даже не зная об этом! Но этого недостаточно. Мы хотим больше. Как насчет модулей, которые могут переключаться между собой? Позвольте расширить нашу небольшую модульную систему, чтобы модули могли фактически не отвечать:
newtype Module m a b =
Module (a -> m (Maybe b, Module m a b))
Это позволяет использовать другую форму композиции, ортогональную (.)
: теперь наш тип также является семейством функторов Alternative
:
instance (Monad m) => Alternative (Module m a)
Теперь модуль может выбрать, отвечать ли на запрос, а если нет, будет проверен следующий модуль. Просто. Вы только что изобрели категорию проводников. =)
Конечно, вам не нужно изобретать это. Библиотека Netwire реализует этот шаблон дизайна и поставляется с большой библиотекой предопределенных "модулей" (называемых проводами). См. Control.Wire для учебника.
Ответ 2
Здесь приводится конкретный пример того, как использовать lens
, как и все остальные. В следующем примере кода Type1
- это локальное состояние (т.е. Ваш молот), а Type2
- глобальное состояние (т.е. Ваш мультиузел). lens
предоставляет функцию zoom
, которая позволяет запускать локализованное вычисление состояния, которое масштабируется в любом поле, определяемом объективом:
import Control.Lens
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State
data Type1 = Type1 {
_field1 :: Int ,
_field2 :: Double}
field1 :: SimpleLens Type1 Int
field1 = lens _field1 (\x a -> x { _field1 = a})
field2 :: SimpleLens Type1 Double
field2 = lens _field2 (\x a -> x { _field2 = a})
data Type2 = Type2 {
_type1 :: Type1 ,
_field3 :: String}
type1 :: SimpleLens Type2 Type1
type1 = lens _type1 (\x a -> x { _type1 = a})
field3 :: SimpleLens Type2 String
field3 = lens _field3 (\x a -> x { _field3 = a})
localCode :: StateT Type1 IO ()
localCode = do
field1 += 3
field2 .= 5.0
lift $ putStrLn "Done!"
globalCode :: StateT Type2 IO ()
globalCode = do
f1 <- zoom type1 $ do
localCode
use field1
field3 %= (++ show f1)
f3 <- use field3
lift $ putStrLn f3
main = runStateT globalCode (Type2 (Type1 9 4.0) "Hello: ")
zoom
не ограничивается непосредственными подполями типа. Поскольку объективы могут быть скомпонованными, вы можете масштабировать столько, сколько хотите в одной операции, просто сделав что-то вроде:
zoom (field1a . field2c . field3b . field4j) $ do ...
Ответ 3
Это очень похоже на применение линз.
Линзы - это спецификация подполя некоторых данных. Идея заключается в том, что у вас есть значение toolLens
и функции view
и set
, поэтому view toolLens :: MultiTool h -> h
извлекает инструмент, а set toolLens :: MultiTool h -> h -> MultiTool h
заменяет его новым значением. Затем вы можете легко определить свой withMember
как функцию, просто принимая объектив.
Технология объективов в последнее время значительно улучшилась, и теперь они невероятно способны. Самая мощная библиотека на момент написания статьи - это библиотека Эдварда Кемата lens
, которая немного глотает, но довольно простой раз вы найдете нужные функции. Вы также можете найти больше вопросов о объективах здесь, на SO, например. Функциональные линзы, которые ссылаются на линзы, fclabels, data-accessor - какая библиотека для доступа к структуре и мутации лучше, или тег lenses.
Ответ 4
Я создал линзируемую расширяемую библиотеку под названием записи данные разнообразно-объектив, который позволяет объединить несколько ReaderT (или StateT), как эта суть:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Control.Lens
import Control.Monad.Reader
import Control.Monad.State
import Data.Diverse.Lens
import Data.Semigroup
foo :: (MonadReader r m, HasItem' Int r, HasItem' String r) => m (Int, String)
foo = do
i <- view (item' @Int) -- explicitly specify type
s <- view item' -- type can also be inferred
pure (i + 10, s <> "bar")
bar :: (MonadState s m, HasItem' Int s, HasItem' String s) => m ()
bar = do
(item' @Int) %= (+10) -- explicitly specify type
item' %= (<> "bar") -- type can also be inferred
pure ()
main :: IO ()
main = do
-- example of running ReaderT with multiple items
(i, s) <- runReaderT foo ((2 :: Int) ./ "foo" ./ nil)
putStrLn $ show i <> s -- prints out "12foobar"
-- example of running StateT with multiple items
is <- execStateT bar ((2 :: Int) ./ "foo" ./ nil)
putStrLn $ show (view (item @Int) is) <> (view (item @String) is) -- prints out "12foobar"
Data.Has - более простая библиотека, которая делает то же самое с кортежами. Пример с главной страницы библиотеки:
{-# LANGUAGE FlexibleContexts #-}
-- in some library code
...
logInAnyReaderHasLogger :: (Has Logger r, MonadReader r m) => LogString -> m ()
logInAnyReaderHasLogger s = asks getter >>= logWithLogger s
queryInAnyReaderHasSQL :: (Has SqlBackEnd r, MonadReader r m) => Query -> m a
queryInAnyReaderHasSQL q = asks getter >>= queryWithSQL q
...
-- now you want to use these effects together
...
logger <- initLogger ...
sql <- initSqlBackEnd ...
('runReader' (logger, sql)) $ do
...
logInAnyReaderHasLogger ...
...
x <- queryInAnyReaderHasSQL ...
...