Подтвердить что ты не робот

Объединение нескольких состояний в 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

Но, конечно, это не скомпилируется.

Я подозреваю, что мое решение слишком объектно-ориентированное. Есть ли способ лучше? Модадские трансформаторы, может быть? Заранее благодарю вас за любые предложения.

4b9b3361

Ответ 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 ...
       ...