Como practicante, ¿por qué debería preocuparme por Haskell? ¿Qué es una mónada y por qué la necesito? [cerrado]

9

Simplemente no entiendo qué problema resuelven.

Trabajo
fuente
1
Casi un duplicado de: programmers.stackexchange.com/questions/25569/…
Orbling
2
Creo que esta edición es un poco extrema. Creo que su pregunta fue esencialmente buena. Es solo que algunas partes eran un poco ... argumentativas. Lo cual probablemente sea solo el resultado de la frustración de tratar de aprender algo que simplemente no estabas viendo.
Jason Baker
@SnOrfus, fui yo quien bastardo la pregunta. Fui demasiado vago para editarlo correctamente.
Trabajo

Respuestas:

34

Las mónadas no son buenas ni malas. Ellos simplemente son. Son herramientas que se utilizan para resolver problemas como muchas otras construcciones de lenguajes de programación. Una aplicación muy importante de ellos es facilitar la vida de los programadores que trabajan en un lenguaje puramente funcional. Pero son útiles en lenguajes no funcionales; es solo que la gente rara vez se da cuenta de que está usando una mónada.

¿Qué es una mónada? La mejor manera de pensar en una mónada es como un patrón de diseño. En el caso de E / S, probablemente podría pensar que es poco más que una tubería glorificada donde el estado global es lo que se está pasando entre las etapas.

Por ejemplo, tomemos el código que está escribiendo:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

Aquí hay mucho más que lo que parece. Por ejemplo, usted notará que putStrLntiene la firma siguiente: putStrLn :: String -> IO (). ¿Por qué es esto?

Piénselo de esta manera: imaginemos (por simplicidad) que stdout y stdin son los únicos archivos en los que podemos leer y escribir. En un lenguaje imperativo, esto no es problema. Pero en un lenguaje funcional, no se puede mutar el estado global. Una función es simplemente algo que toma un valor (o valores) y devuelve un valor (o valores). Una forma de evitar esto es usar el estado global como el valor que se transfiere dentro y fuera de cada función. Para que pueda traducir la primera línea de código en algo como esto:

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

... y el compilador sabría imprimir cualquier cosa que se agregue al segundo elemento de global_state. Ahora no sé sobre ti, pero odiaría programar así. La forma en que esto se hizo más fácil fue usar Mónadas. En una mónada, pasa un valor que representa algún tipo de estado de una acción a la siguiente. Es por eso que putStrLntiene un tipo de retorno de IO (): está devolviendo el nuevo estado global.

Entonces, ¿por qué te importa? Bueno, las ventajas de la programación funcional sobre el programa imperativo se han debatido hasta la muerte en varios lugares, por lo que no voy a responder esa pregunta en general (pero vea este documento si desea conocer el caso de la programación funcional). Sin embargo, para este caso específico, podría ayudar si entendiera lo que Haskell está tratando de lograr.

Muchos programadores sienten que Haskell intenta evitar que escriban códigos imperativos o que usen efectos secundarios. Eso no es del todo cierto. Piénselo de esta manera: un lenguaje imperativo es aquel que permite efectos secundarios por defecto, pero le permite escribir código funcional si realmente lo desea (y está dispuesto a lidiar con algunas de las contorsiones que requeriría). Haskell es puramente funcional por defecto, pero le permite escribir código imperativo si realmente lo desea (lo que debe hacer si su programa es útil). El punto no es hacer que sea difícil escribir código que tenga efectos secundarios. Es para asegurarse de que sea explícito acerca de tener efectos secundarios (con el sistema de tipos que lo impone).

Jason Baker
fuente
66
Ese último párrafo es oro. Para extraerlo y parafrasearlo un poco: "Un lenguaje imperativo es aquel que permite efectos secundarios por defecto, pero le permite escribir código funcional si realmente lo desea. Un lenguaje funcional es puramente funcional por defecto, pero le permite escribir código imperativo Si de verdad quieres."
Frank Shearar
Vale la pena señalar que el documento al que se vinculó rechaza específicamente la idea de "inmutabilidad como una virtud de la programación funcional" desde el principio.
Mason Wheeler
@MasonWheeler: Leí esos párrafos, no como descartando la importancia de la inmutabilidad, sino rechazándolo como un argumento convincente para demostrar la superioridad de la programación funcional. De hecho, dice lo mismo sobre la eliminación de goto(como argumento para la programación estructurada) un poco más adelante en el documento, caracterizando tales argumentos como "infructuosos". Y sin embargo, ninguno de nosotros secretamente desea gotoel regreso. Es simplemente que no puede argumentar que gotono es necesario para las personas que lo usan ampliamente.
Robert Harvey
7

¡¡¡Morderé!!! Las mónadas por sí mismas no son realmente una razón de ser para Haskell (las primeras versiones de Haskell ni siquiera las tenían).

Su pregunta es un poco como decir "C ++ cuando miro la sintaxis, me aburro mucho. Pero las plantillas son una característica muy publicitada de C ++, así que miré una implementación en otro lenguaje".

La evolución de un programador Haskell es una broma, no debe tomarse en serio.

Una Mónada para el propósito de un programa en Haskell es una instancia de la clase de tipo Mónada, es decir, es un tipo que soporta cierto conjunto pequeño de operaciones. Haskell tiene soporte especial para tipos que implementan la clase de tipo Monad, específicamente soporte sintáctico. Prácticamente, esto resulta en lo que se ha denominado un "punto y coma programable". Cuando combina esta funcionalidad con algunas de las otras características de Haskell (funciones de primera clase, pereza por defecto), lo que obtiene es la capacidad de implementar ciertas cosas como bibliotecas que tradicionalmente se han considerado características del lenguaje. Puede, por ejemplo, implementar un mecanismo de excepción. Puede implementar el soporte para continuaciones y corutinas como una biblioteca. Haskell, el lenguaje no tiene soporte para variables mutables:

Usted pregunta acerca de "¿Quizás / Identidad / Mónadas División Segura ???". La mónada Quizás es un ejemplo de cómo podría implementar (muy simple, solo una excepción) el manejo de excepciones como una biblioteca.

Tienes razón, escribir mensajes y leer los comentarios de los usuarios no es muy exclusivo. IO es un pésimo ejemplo de "mónadas como característica".

Pero para iterar, una "característica" en sí misma (por ejemplo, Mónadas) aislada del resto del lenguaje no necesariamente parece útil de inmediato (una gran característica nueva de C ++ 0x son las referencias de valor, no significa que pueda tomar fuera del contexto C ++ porque su sintaxis te aburre y necesariamente ve la utilidad). Un lenguaje de programación no es algo que obtienes al lanzar un montón de características en un cubo.

Logan Capaldo
fuente
En realidad, haskell tiene soporte para variables mutables a través de la mónada ST (una de las pocas partes mágicas impuras extrañas del lenguaje que juega según sus propias reglas).
sara
4

Todos los programadores escriben programas, pero las similitudes terminan ahí. Creo que los programadores difieren mucho más de lo que la mayoría de los programadores pueden imaginar. Tome cualquier "batalla" de larga data, como escribir variables estáticas frente a tipos de solo tiempo de ejecución, scripting vs compilados, estilo C vs orientado a objetos. Le resultará imposible argumentar racionalmente que un campo es inferior, porque algunos de ellos producen un código excelente en algún sistema de programación que me parece inútil o incluso inutilizable.

Creo que diferentes personas piensan de manera diferente, y si no te sientes tentado por el azúcar sintáctico o especialmente por las abstracciones que solo existen por conveniencia y que en realidad tienen un costo de tiempo de ejecución significativo, entonces mantente alejado de esos idiomas.

Sin embargo, le recomendaría que al menos intente familiarizarse con los conceptos que está renunciando. No tengo nada en contra de alguien que es vehementemente pro-puro-C, siempre y cuando realmente entiendan de qué se trata la expresión lambda. Sospecho que la mayoría no se convertirá inmediatamente en un fanático, pero al menos estará allí en el fondo de sus mentes cuando encuentren el problema perfecto, lo que hubiera sido mucho más fácil de resolver con lambdas.

Y, sobre todo, trata de evitar molestarte con fanboy speak, especialmente por personas que realmente no saben de qué están hablando.

Roman Starkov
fuente
4

Haskell impone la transparencia referencial : dados los mismos parámetros, cada función siempre devuelve el mismo resultado, sin importar cuántas veces llame a esa función.

Eso significa, por ejemplo, que en Haskell (y sin Mónadas) no puede implementar un generador de números aleatorios. En C ++ o Java puede hacerlo usando variables globales, almacenando el valor intermedio "semilla" del generador aleatorio.

En Haskell, la contraparte de las variables globales son las mónadas.

vz0
fuente
Entonces ... ¿y si quisieras un generador de números aleatorios? ¿No es también una función? Incluso si no, ¿cómo consigo un generador de números aleatorios?
Trabajo
@Job Puedes hacer un generador de números aleatorios dentro de una mónada (es básicamente un rastreador de estado), o puedes usar unsafePerformIO, el demonio de Haskell que nunca debería usarse (y de hecho probablemente romperá tu programa si usas aleatoriedad dentro de él!)
alternativa
En Haskell, puedes pasar un 'RandGen' que es básicamente el estado actual del RNG. Entonces, la función que genera un nuevo número aleatorio toma un RandGen y devuelve una tupla con el nuevo RandGen y el número producido. La alternativa es especificar en algún lugar que desea una lista de números aleatorios entre un valor mínimo y máximo. Esto devolverá un flujo infinito de números evaluado de manera perezosa, por lo que podemos recorrer esta lista siempre que necesitemos un nuevo número aleatorio.
Qqwy
¡De la misma manera que los obtienes en cualquier otro idioma! obtienes un algoritmo generador de números pseudoaleatorios, y luego lo siembras con algún valor y cosechas los números "aleatorios" que aparecen. La única diferencia es que lenguajes como C # y Java sembran automáticamente el PRNG para usted usando el reloj del sistema o cosas así. Eso y el hecho de que en haskell también obtienes un nuevo PRNG que puedes usar para obtener el "siguiente" número, mientras que en C # / Java, todo esto se hace internamente usando variables mutables en el Randomobjeto.
sara
4

Es una pregunta vieja, pero es muy buena, así que responderé.

Puede pensar en las mónadas como bloques de código para los que tiene un control completo sobre cómo se ejecutan: qué debe devolver cada línea de código, si la ejecución debe detenerse en algún momento, si debe ocurrir algún otro procesamiento entre cada línea.

Daré algunos ejemplos de cosas que las mónadas permiten que de otra forma serían difíciles. Ninguno de estos ejemplos está en Haskell, solo porque mi conocimiento de Haskell es un poco inestable, pero todos son ejemplos de cómo Haskell ha inspirado el uso de mónadas.

Analizadores

Normalmente, si quisiera escribir un analizador de algún tipo, por ejemplo, para implementar un lenguaje de programación, tendría que leer la especificación BNF y escribir un montón de código de bucle para analizarlo, o tendría que usar un compilador de compilador como Flex, Bison, yacc, etc. Pero con las mónadas, puedes hacer una especie de "analizador de compiladores" directamente en Haskell.

Los analizadores realmente no se pueden hacer sin mónadas o lenguajes especiales como yacc, bison, etc.

Por ejemplo, tomé la especificación del lenguaje BNF para el protocolo IRC :

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

Y lo redujo a aproximadamente 40 líneas de código en F # (que es otro lenguaje que admite mónadas):

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

La sintaxis de mónada de F # es bastante fea en comparación con la de Haskell, y probablemente podría haber mejorado esto bastante, pero el punto a tener en cuenta es que, estructuralmente, el código del analizador es idéntico al BNF. Esto no solo habría llevado mucho más trabajo sin mónadas (o un generador de analizadores sintácticos), sino que habría tenido casi ninguna semejanza con la especificación y, por lo tanto, habría sido terrible tanto para leer como para mantener.

Multitarea personalizada

Normalmente, se considera que la multitarea es una característica del sistema operativo, pero con las mónadas, puede escribir su propio planificador de modo que después de cada mónada de instrucciones, el programa pasaría el control al planificador, que luego elegiría otra mónada para ejecutar.

Un tipo hizo una mónada de "tarea" para controlar los bucles del juego (nuevamente en F #), de modo que en lugar de tener que escribir todo como una máquina de estado que actúa en cada Update()llamada, podría escribir todas las instrucciones como si fueran una sola función .

En otras palabras, en lugar de tener que hacer algo como:

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

Podrías hacer algo como:

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ to SQL

LINQ to SQL es en realidad un ejemplo de una mónada, y una funcionalidad similar podría implementarse fácilmente en Haskell.

No entraré en detalles ya que no recuerdo con tanta precisión, pero Erik Meijer lo explica muy bien .

Rei Miyasaka
fuente
1

Si está familiarizado con los patrones GoF, las mónadas son como el patrón Decorator y el patrón Builder juntos, con esteroides, mordidos por un tejón radiactivo.

Hay mejores respuestas arriba, pero algunos de los beneficios específicos que veo son:

  • Las mónadas decoran algún tipo de núcleo con propiedades adicionales sin cambiar el tipo de núcleo. Por ejemplo, una mónada podría "levantar" una cadena y agregar valores como "isWellFormed", "isProfanity" o "isPalindrome", etc.

  • Del mismo modo, las mónadas permiten conglomerar un tipo simple en un tipo de colección

  • las mónadas permiten la unión tardía de funciones en este espacio de orden superior

  • las mónadas permiten mezclar funciones y argumentos arbitrarios con un tipo de datos arbitrario, en el espacio de orden superior

  • las mónadas permiten mezclar funciones puras y sin estado con una base impura y con estado, para que pueda realizar un seguimiento de dónde está el problema

Un ejemplo familiar de una mónada en Java es List. Toma alguna clase de núcleo, como String, y la "eleva" al espacio de mónada de List, agregando información sobre la lista. Luego vincula nuevas funciones en ese espacio como get (), getFirst (), add (), empty (), etc.

A gran escala, imagine que en lugar de escribir un programa, simplemente escribió un gran generador (como el patrón GoF), y el método build () al final escupió cualquier respuesta que se suponía que debía producir el programa. Y que podría agregar nuevos métodos a su ProgramBuilder sin volver a compilar el código original. Es por eso que las mónadas son un modelo de diseño poderoso.

Robar
fuente