design-patterns haskell types monad-transformers

design patterns - ¿Cómo diseñar una pila monádica?



design-patterns haskell (2)

¿Cómo diseñas y construyes tus pilas monádicas? Por primera vez necesito construir una pila monádica (utilizando transformadores) para resolver un problema del mundo real, pero no estoy completamente seguro en qué orden apilar los transformadores. Como ya sabe, mientras un cálculo tenga una especie * -> * , básicamente cualquier cosa puede desempeñar el papel de la mónada interna en un transformador, por lo tanto, un par de preguntas:

  • ¿Debería algún transformador en particular estar en la parte superior de la pila (por ejemplo, ReaderT? WriterT?)
  • ¿Qué debe impulsar el diseño? ¿Intuición? ¿Tipos? (por ejemplo, moldea la pila de acuerdo a las necesidades de tu API)
  • Es cada pila isomorfa entre sí (hasta cierto punto) o es probable que, si construyo mi pila incorrectamente, podría terminar por no poder usar ciertas mónadas subyacentes o tener un gran desorden de lift . lift . liftIO [...] lift . lift . liftIO [...] lift . lift . liftIO [...] ? Mi intuición sugiere que, si los transformadores derivan algunos casos (por ejemplo, MonadReader, MonadIO, etc., como la mayoría de los transformadores en mtl ), no debería importar en qué orden coloco los transformadores.

Estoy interesado en escuchar a los experimentados Haskellers sobre las mejores prácticas o las reglas prácticas.

forever $ print "Thanks!"

A.


Esta es una pregunta bastante amplia. Solo te voy a dar algunas ideas básicas para trabajar.

En primer lugar, sugiero mantener la base monádica polimórfica siempre que sea posible. Esto le permitirá reutilizar el código tanto en la configuración pura como en la de IO. Esto también hará que tu código sea más componible. El uso de varias clases como MonadIO también puede ayudar a mantener su código más polimórfico, lo que generalmente es bueno.

Una cosa importante a tener en cuenta es que el orden de los transformadores de mónada en realidad controla su semántica. Mi ejemplo favorito es combinar algo como ListT ¹ con EitherT para el manejo de errores. Si tiene el ListT en el exterior, todo el cálculo puede fallar con un error. Si tiene EitherT en el exterior, entonces cada rama puede fallar por separado. ¡Así que realmente puede controlar la forma en que los errores interactúan con el no determinismo simplemente cambiando el orden de sus transformadores!

Si los transformadores de mónada que está utilizando no dependen del orden (por ejemplo, no importará mucho la combinación de ReaderT y WriterT , creo), entonces juegue de oreja e vaya con lo que parezca mejor para su aplicación. Este es el tipo de elección que se hará más fácil con la experiencia.

: ListT de Control.Monad.Trans tiene algunos problemas, así que supongamos que ListT hace correctamente .


Se necesita experiencia. Una cosa para recordar es que el transformador de la mónada no sabe nada acerca de la mónada que está transformando, por lo que el externo está "atado" por el comportamiento del interno. Asi que

StateT s (ListT m) a

Es, ante todo, un cálculo no determinista debido a la mónada interna. Luego, tomando el no determinismo como normal, agrega un estado, es decir, cada "rama" del no determinismo tendrá su propio estado.

Restricción con ListT (StateT sm) a , que es principalmente con estado, es decir, solo habrá un estado para todo el cómputo (módulo m ), y el cálculo actuará como "un solo hilo" en el estado, porque eso es lo que significa State . El no determinismo estará por encima de eso, por lo que las sucursales podrán observar los cambios de estado de las ramas fallidas anteriores. (En esta combinación en particular, eso es realmente extraño, y nunca lo he necesitado).

Aquí hay un diagram de Dan Piponi que proporciona una intuición útil:

También me resulta útil expandirme al tipo de implementación, para darme una idea de qué tipo de cálculo es. ListT es difícil de expandir, pero usted puede verlo como "nondeterminsm" y StateT es fácil de expandir. Así que para el ejemplo anterior, me gustaría ver

StateT s (ListT m) a =~ s -> ListT m (a,s)

Es decir, toma un estado entrante y devuelve muchos estados salientes. Esto te da una idea de cómo va a funcionar. Un enfoque similar es observar el tipo de función de run que necesitaría para su pila. ¿Coincide esto con la información que tiene y la información que necesita?

Aquí hay algunas reglas de oro. No son un sustituto para tomarse el tiempo de averiguar cuál realmente necesita al expandirse y buscar, pero si solo está buscando "agregar características" en una especie de sentido imperativo, esto podría ser útil.

ReaderT , WriterT y StateT son los transformadores más comunes. Primero, todos viajan entre sí, por lo que es irrelevante el orden en el que los pongas (considera usar RWS si estás usando los tres). Además, en la práctica, por lo general los quiero en el exterior, con transformadores "más ricos" como ListT , LogicT y ContT en el interior.

ErrorT y MaybeT usualmente van fuera de los tres anteriores; echemos un vistazo a cómo MaybeT interactúa con StateT :

MaybeT (StateT s m) a =~ StateT s m (Maybe a) =~ s -> m (Maybe a, s) StateT s (MaybeT m) a =~ s -> MaybeT m (a,s) =~ s -> m (Maybe (a,s))

Cuando MaybeT está en el exterior, se puede observar un cambio de estado incluso si falla el cálculo. Cuando MaybeT está en el interior, si el cálculo falla, no se obtiene un estado, por lo que debe abortar cualquier cambio de estado que haya ocurrido en el cálculo fallido. Cuál de estos desea depende de lo que intenta hacer; el primero, sin embargo, corresponde a las intuiciones imperativas de los programadores. (No es que sea algo por lo que hay que luchar)

Espero que esto te haya dado una idea de cómo pensar acerca de las pilas de transformadores, para que tengas más herramientas para analizar cómo debería verse tu pila. Si identifica un problema como un cálculo monádico, hacer que la mónada sea correcta es una de las decisiones más importantes que debe tomar, y no siempre es fácil. Tómate tu tiempo y explora las posibilidades.