haskell monads dsl free-monad

haskell - ¿Cuándo quisiera usar un patrón de Monad+intérprete gratis?



monads dsl (1)

Estoy trabajando en un proyecto que, entre otras cosas, implica una capa de acceso a la base de datos. Bastante normal, realmente. En un proyecto anterior, un colaborador me animó a usar el concepto de Mónadas Libres para una capa de base de datos, y así lo hice. Ahora estoy tratando de decidir en mi nuevo proyecto qué es lo que gano.

En el proyecto anterior, tenía una API que se parecía bastante a esto.

saveDocument :: RawDocument -> DBAction () getDocuments :: DocumentFilter -> DBAction [RawDocument] getDocumentStats :: DBAction [(DocId, DocumentStats)]

etc. Alrededor de veinte de esas funciones públicas. Para apoyarlos, tenía la estructura de datos DBAction :

data DBAction a = SaveDocument RawDocument (DBAction a) | GetDocuments DocumentFilter ([RawDocument] -> DBAction a) | GetDocumentStats ([(DocId, DocumentStats)] -> DBAction a) | Return a

Y luego una implementación de mónada:

instance Monad DBAction where return = Return SaveDocument doc k >>= f = SaveDocument doc (k >>= f) GetDocuments df k >>= f = GetDocuments df (k >=> f)

Y luego el intérprete. Y luego las funciones primitivas que implementan cada una de las diferentes consultas. Básicamente, siento que tengo una gran cantidad de código de pegamento.

En mi proyecto actual (en un campo totalmente diferente), me he ido con una mónada bastante común para mi base de datos:

newtype DBM err a = DBM (ReaderT DB (EitherT err IO) a) deriving (Monad, MonadIO, MonadReader DB) indexImage :: (ImageId, UTCTime) -> Exif -> Thumbnail -> DBM SaveError () removeImage :: DB -> ImageId -> DBM DeleteError ()

Y así. Me imagino que, en última instancia, tendré las funciones "públicas" que representan conceptos de alto nivel que se ejecutan en el contexto de DBM , y luego tendré toda la serie de funciones que hacen el pegamento SQL / Haskell. Esto es, en general, sentirse mucho mejor que el sistema de mónadas gratuito porque no estoy escribiendo una gran cantidad de código repetitivo para no obtener nada más que la capacidad de cambiar mi intérprete.

O...

¿De verdad gané algo más con el patrón Free Monad + Interpreter? ¿Entonces qué?


Como se menciona en los comentarios, a menudo es deseable tener alguna abstracción entre el código y la implementación de la base de datos. Puedes obtener la misma abstracción que una mónada libre definiendo una clase para tu DB Monad (me he tomado algunas libertades aquí):

class (Monad m) => MonadImageDB m where indexImage :: (ImageId, UTCTime) -> Exif -> Thumbnail -> m SaveResult removeImage :: ImageId -> m DeleteResult

Si su código está escrito contra MonadImageDB m => lugar de estar estrechamente acoplado a DBM , podrá cambiar la base de datos y el manejo de errores sin modificar su código.

¿Por qué usarías gratis en su lugar? Porque "libera al intérprete tanto como sea posible" , lo que significa que el intérprete solo se compromete a proporcionar una mónada y nada más. Esto significa que usted no tiene restricciones para escribir instancias de mónada para su código. Tenga en cuenta que, para la mónada gratuita, no escribe su propia instancia para Monad , la obtiene de forma gratuita . Escribirías algo como

data DBActionF next = SaveDocument RawDocument ( next) | GetDocuments DocumentFilter ([RawDocument] -> next) | GetDocumentStats ([(DocId, DocumentStats)] -> next)

deriva Functor DBActionF , y obtén la instancia de mónada para Free DBActionF de la instancia existente para Functor f => Monad (Free f) .

Para su ejemplo, sería en cambio:

data ImageActionF next = IndexImage (ImageId, UTCTime) Exif Thumbnail (SaveResult -> next) | RemoveImage ImageId (DeleteResult -> next)

También puede obtener la propiedad "libera al intérprete tanto como sea posible" para la clase de tipo. Si no tiene otras restricciones en m que en la clase de tipo, MonadImageDB y todos los métodos de MonadImageDB podrían ser constructores para un Functor , entonces obtendrá la misma propiedad. Puede ver esto implementando la instance MonadImageDB (Free ImageActionF) .

Si va a mezclar su código con interacciones con alguna otra mónada, puede obtener un transformador de mónada de forma gratuita en lugar de una mónada.

Elegir

No tienes que elegir. Puede convertir de ida y vuelta entre las representaciones. Este ejemplo muestra cómo hacerlo para acciones con cero, uno o dos argumentos que devuelven cero, uno o dos resultados. Primero, un poco de repetición

{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE FlexibleInstances #-} import Control.Monad.Free

Tenemos una clase de tipo

class Monad m => MonadAddDel m where add :: String -> m Int del :: Int -> m () set :: Int -> String -> m () add2 :: String -> String -> m (Int, Int) nop :: m ()

y una representación de functor equivalente

data AddDelF next = Add String ( Int -> next) | Del Int ( next) | Set Int String ( next) | Add2 String String (Int -> Int -> next) | Nop ( next) deriving (Functor)

La conversión de la representación libre a la clase de tipo reemplaza a Pure con return , Free con >>= , Add with add , etc.

run :: MonadAddDel m => Free AddDelF a -> m a run (Pure a) = return a run (Free (Add x next)) = add x >>= run . next run (Free (Del id next)) = del id >> run next run (Free (Set id x next)) = set id x >> run next run (Free (Add2 x y next)) = add2 x y >>= /ids -> run (next (fst ids) (snd ids)) run (Free (Nop next)) = nop >> run next

Una instancia de MonadAddDel para la representación crea funciones para los next argumentos de los constructores que usan Pure .

instance MonadAddDel (Free AddDelF) where add x = Free . (Add x ) $ Pure del id = Free . (Del id ) $ Pure () set id x = Free . (Set id x) $ Pure () add2 x y = Free . (Add2 x y) $ /id1 id2 -> Pure (id1, id2) nop = Free . Nop $ Pure ()

(Ambos tienen patrones que podríamos extraer para el código de producción, la parte más difícil para escribirlos genéricamente sería lidiar con la cantidad variable de argumentos de entrada y resultado)

La codificación en contra de la clase de tipo solo usa la MonadAddDel m => , por ejemplo:

example1 :: MonadAddDel m => m () example1 = do id <- add "Hi" del id nop (id3, id4) <- add2 "Hello" "World" set id4 "Again"

Era demasiado perezoso para escribir otra instancia para MonadAddDel además del que obtuve de forma gratuita, y demasiado perezoso para hacer un ejemplo además de utilizar la clase de tipo MonadAddDel .

Si desea ejecutar un código de ejemplo, esto es suficiente para ver el ejemplo interpretado una vez (convirtiendo la representación de clase de tipo a la representación libre), y de nuevo después de convertir nuevamente la representación libre de nuevo a la representación de clase de tipo. De nuevo, soy demasiado flojo para escribir el código dos veces.

debugInterpreter :: Free AddDelF a -> IO a debugInterpreter = go 0 where go n (Pure a) = return a go n (Free (Add x next)) = do print $ "Adding " ++ x ++ " with id " ++ show n go (n+1) (next n) go n (Free (Del id next)) = do print $ "Deleting " ++ show id go n next go n (Free (Set id x next)) = do print $ "Setting " ++ show id ++ " to " ++ show x go n next go n (Free (Add2 x y next)) = do print $ "Adding " ++ x ++ " with id " ++ show n ++ " and " ++ y ++ " with id " ++ show (n+1) go (n+2) (next n (n+1)) go n (Free (Nop next)) = do print "Nop" go n next main = do debugInterpreter example1 debugInterpreter . run $ example1