haskell - leibniz - Parsec: Aplicativos vs Mónadas
monadas filosofia (3)
Estoy empezando con Parsec (con poca experiencia en Haskell), y estoy un poco confundido sobre el uso de mónadas o aplicativos. La sensación general que tuve después de leer "Real World Haskell", "Write You a Haskell" y una pregunta aquí es que se prefieren los aplicativos, pero realmente no tengo idea.
Así que mis preguntas son:
- ¿Qué enfoque se prefiere?
- ¿Se pueden mezclar las mónadas y los aplicativos (usarlos cuando son más útiles que los otros)?
- Si la última respuesta es sí, ¿debo hacerlo?
En general, comience con lo que tenga más sentido para usted. Después considere lo siguiente.
Es una buena práctica usar el Applicative
(o incluso el Functor
) cuando sea posible. En general, es más fácil para un compilador como GHC optimizar estas instancias, ya que pueden ser más simples que Monad
. Creo que el consejo general de la comunidad después de AMP ha sido hacer que sus limitaciones sean lo más generales posible. Recomendaría el uso de la extensión GHC ApplicativeDo
ya que puede usar la notación do
uniforme y solo obtener una restricción de Applicative
cuando eso es todo lo que se necesita.
Dado que el tipo de analizador ParsecT
es una instancia tanto de Applicative
como de Monad
, puede mezclar y combinar los dos. Hay situaciones en las que hacer esto es más legible: todo depende de la situación.
También, considere usar megaparsec
. megaparsec
es una bifurcación de parsec
más reciente, mantenida de forma más activa y generalmente más limpia.
EDITAR
Dos cosas que, al releer mi respuesta y los comentarios, realmente no hice un buen trabajo de aclaración:
El principal beneficio de utilizar
Applicative
es que para muchos tipos admite implementaciones mucho más eficientes (por ejemplo,(<*>)
es más eficaz queap
).Si solo desea escribir algo como
(+) <$> parseNumber <*> parseNumber
, no hay necesidad de ingresar aApplicativeDo
- sería más detallado. UsaríaApplicativeDo
solo cuando empiezas a encontrarte escribiendo expresiones aplicativas muy largas o anidadas.
Podría valer la pena prestar atención a la diferencia semántica clave entre el Applicative
y la Monad
, para determinar cuándo es apropiado. Comparar tipos:
(<*>) :: m (s -> t) -> m s -> m t
(>>=) :: m s -> (s -> m t) -> m t
Para implementar <*>
, elige dos cómputos, uno de una función, el otro de un argumento, luego sus valores se combinan por aplicación. Para implementar >>=
, elige un cálculo y explica cómo utilizará los valores resultantes para elegir el siguiente cálculo. Es la diferencia entre el "modo por lotes" y la operación "interactiva".
Cuando se trata de analizar, Applicative
(extendido con el fracaso y la opción de dar Alternative
) captura los aspectos sin contexto de su gramática. Necesitará la potencia adicional que Monad
le proporciona solo si necesita inspeccionar el árbol de análisis de parte de su entrada para decidir qué gramática debe usar para otra parte de su entrada. Por ejemplo, puede leer un descriptor de formato y luego una entrada en ese formato. Minimizar el uso del poder adicional de las mónadas le indica qué dependencias de valor son esenciales.
Al pasar del análisis al paralelismo, esta idea de usar >>=
solo para la dependencia esencial del valor le brinda claridad sobre las oportunidades para distribuir la carga. Cuando dos cálculos se combinan con <*>
, ninguno necesita esperar al otro. Aplicativo-cuando-puedes-pero-monádico-cuando-debes-es la fórmula para la velocidad. El punto de ApplicativeDo
es automatizar el análisis de dependencia del código que se ha escrito en un estilo monádico y, por lo tanto, se ha sobrepasado de forma accidental.
Su pregunta también se relaciona con el estilo de codificación, sobre qué opiniones son libres de diferir. Pero déjame contarte una historia. Vine a Haskell desde Standard ML, donde estaba acostumbrado a escribir programas en estilo directo, incluso si hacían cosas malas como lanzar excepciones o mutar referencias. ¿Qué estaba haciendo en ML? Trabajando en una implementación de una teoría de tipo ultra-pura (que no puede ser nombrada, por razones legales). Cuando trabajaba en esa teoría de tipos, no podía escribir programas de estilo directo que usaran excepciones, pero preparé los combinadores aplicativos como una forma de acercarme lo más posible al estilo directo.
Cuando me mudé a Haskell, me horrorizaba descubrir hasta qué punto las personas parecían pensar que la programación en notación pseudo-imperativa era solo un castigo por la impureza semántica más leve (aparte, por supuesto, de la no terminación). Adopté los combinadores aplicativos como una elección de estilo (y me acerqué aún más al estilo directo con "corchetes idiomáticos") mucho antes de comprender la distinción semántica, es decir, que representaban un debilitamiento útil de la interfaz de la mónada. Simplemente no me gustó (y todavía no me gusta) la forma en que la notación de hacer requiere la fragmentación de la estructura de la expresión y el nombramiento gratuito de las cosas.
Es decir, las mismas cosas que hacen que el código funcional sea más compacto y legible que el código imperativo también hacen que el estilo aplicativo sea más compacto y legible que la notación por hacer. Aprecio que ApplicativeDo
es una excelente manera de crear programas más aplicativos (y en algunos casos eso significa más rápido ) que fueron escritos en un estilo monádico que no tiene tiempo de refactorizar. Pero de lo contrario, diría que aplicativo-cuando-puedes-pero-monádico-cuando-debes-también es la mejor manera de ver lo que está pasando.
Siguiendo con @pigworker (soy demasiado nuevo aquí para comentar, lamentablemente) vale la pena mencionar join $ fM <*> ... <*> ... <*> ...
como patrón. Le ofrece una familia "bind1, bind2, bind3 ..." de la misma manera que <$>
y <*>
le proporciona una "fmap1, fmap2, fmap3".
Como cosa estilística, cuando está lo suficientemente acostumbrado a los combinadores, es posible que do
lo mismo que usaría let
: como una forma de resaltar cuando quería nombrar algo. Tiendo a querer nombrar cosas con más frecuencia en los analizadores, por ejemplo, ¡porque eso probablemente corresponde a los nombres en la especificación!