haskell state

haskell - Combinando mĂșltiples estados en StateT



tree haskell (4)

Aquí hay un ejemplo concreto de cómo usar lens como todos los demás están hablando. En el siguiente ejemplo de código, Type1 es el estado local (es decir, su martillo), y Type2 es el estado global (es decir, su multiherramienta). lens proporciona la función de zoom que le permite ejecutar un cálculo de estado localizado que zoom cualquier campo definido por una lente:

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 no se limita a los subcampos inmediatos de un tipo. Dado que las lentes son compactables, puede hacer el zoom a la profundidad que desee en una sola operación simplemente haciendo algo como:

zoom (field1a . field2c . field3b . field4j) $ do ...

Estoy escribiendo un programa que se ejecuta como un demonio. Para crear el demonio, el usuario proporciona un conjunto de implementaciones para cada una de las clases requeridas (una de ellas es una base de datos). Todas estas clases tienen funciones con firmas de tipo con el formato StateT s IO a , pero s es diferente para cada clase .

Supongamos que cada una de las clases sigue este patrón:

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.

Ahora puedo definir un registro que representa la implementación elegida por el usuario para cada "ranura".

data MultiTool h = MultiTool { hammer :: h -- Plus additional fields for wrenches, screwdrivers, etc. }

Y el daemon realiza la mayor parte de su trabajo en la mónada StateT (MultiTool h ...) IO () .

Ahora, dado que la multiherramienta contiene un martillo, puedo usarlo en cualquier situación donde se necesite un martillo. En otras palabras, el tipo MultiTool puede implementar cualquiera de las clases que contiene, si escribo un código como este:

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

Pero las implementaciones de withHammer , withWrench , withScrewdriver , etc. son básicamente idénticas. Sería bueno poder escribir algo como esto ...

--withMember accessor runProgram = do -- u <- get -- stateMap (/h -> u {accessor=h}) accessor runProgram -- instance Hammer h => Hammer (MultiTool h) where -- driveNail = withMember hammer driveNail

Pero claro que eso no compilará.

Sospecho que mi solución está demasiado orientada a objetos. ¿Hay alguna manera mejor? Transformadores de mónada, tal vez? Gracias de antemano por cualquier sugerencia.


Creé una biblioteca de registros extensible con lentes llamada data-diverse-lens que permite combinar múltiples ReaderT (o StateT) como esta gist :

{-# 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 es una biblioteca más simple que hace lo mismo con tuplas. Ejemplo de la portada de la biblioteca:

{-# 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 ... ...


Esto suena muy parecido a una aplicación de lentes.

Las lentes son una especificación de un subcampo de algunos datos. La idea es que tengas algún valor en la view y set toolLens y las herramientas, de modo que la view toolLens :: MultiTool h -> h recupera la herramienta y set toolLens :: MultiTool h -> h -> MultiTool h la herramienta set toolLens :: MultiTool h -> h -> MultiTool h reemplaza con un nuevo valor. Luego, puede definir fácilmente su withMember como una función que solo acepta una lente.

La tecnología de lentes ha avanzado mucho recientemente, y ahora son increíblemente capaces. La biblioteca más poderosa que existe en el momento de escribir es la biblioteca de lens Edward Kmett, que es mucho más que tragar, pero bastante simple una vez que encuentre las funciones que desea. También puede buscar más preguntas sobre lentes aquí en SO, por ejemplo, lentes funcionales que se vinculan con lentes, etiquetas, accesores de datos, qué biblioteca para el acceso de la estructura y la mutación es mejor , o la etiqueta de lenses .


Si desea ir con un gran estado global como en su caso, entonces lo que quiere usar es lentes, como lo sugiere Ben. Yo también recomiendo la biblioteca de lentes de Edward Kmett. Sin embargo, hay otra forma, quizás mejor.

Los servidores tienen la propiedad de que el programa se ejecuta continuamente y realiza la misma operación en un espacio de estado. El problema comienza cuando desea modularizar su servidor, en cuyo caso desea algo más que un estado global. Quieres que los módulos tengan su propio estado.

Pensemos en un módulo como algo que transforma una solicitud de respuesta :

Module :: (Request -> m Response) -> Module m

Ahora, si tiene algún estado, entonces este estado se vuelve notable en que el módulo podría dar una respuesta diferente la próxima vez. Hay varias formas de hacerlo, por ejemplo, las siguientes:

Module :: s -> ((Request, s) -> m (Response s)) -> Module m

Pero una forma mucho más agradable y equivalente de expresar esto es el siguiente constructor (pronto construiremos un tipo a su alrededor):

Module :: (Request -> m (Response, Module m)) -> Module m

Este módulo asigna una solicitud a una respuesta, pero en el camino también devuelve una nueva versión de sí mismo. Vayamos más lejos y hagamos peticiones y respuestas polimórficas:

Module :: (a -> m (b, Module m a b)) -> Module m a b

Ahora, si el tipo de salida de un módulo coincide con el tipo de entrada de otro módulo, entonces puede componerlos como funciones normales. Esta composición es asociativa y tiene una identidad polimórfica. Esto suena mucho como una categoría, y de hecho lo es! Es una categoría, un funtor aplicativo y una flecha.

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)

¡Ahora podemos componer dos módulos que tienen su propio estado local sin siquiera saberlo! Pero eso no es suficiente. Queremos más. ¿Qué hay de los módulos que se pueden cambiar entre? Extendamos nuestro pequeño sistema de módulos de modo que los módulos realmente puedan elegir no dar una respuesta:

newtype Module m a b = Module (a -> m (Maybe b, Module m a b))

Esto permite otra forma de composición que es ortogonal a (.) : Ahora nuestro tipo también es una familia de funtores Alternative :

instance (Monad m) => Alternative (Module m a)

Ahora, un módulo puede elegir si responder a una solicitud, y si no, se intentará con el siguiente módulo. Sencillo. Acabas de reinventar la categoría de cables. =)

Por supuesto que no necesitas reinventar esto. La biblioteca Netwire implementa este patrón de diseño y viene con una gran biblioteca de "módulos" predefinidos (llamados cables). Vea el módulo Control.Wire para un tutorial.