Usar return vs. no usar return en la mónada de lista

Comencé mi Grand Haskell Crusade (GHC :)) y estoy un poco confundido con las mónadas y las funciones IO. ¿Alguien podría explicar simplemente ¿Cuál es la diferencia entre esas dos funciones?

f1 = do x <- [1,2]
        [x, x+1] -- this is monad, right?

f2 = do x <- [1,2]
        return [x, x+1]

Los resultados son:

*Main> f1
[1,2,2,3]

*Main> f2
[[1,2],[2,3]]

preguntado el 04 de julio de 12 a las 07:07

Además de estas excelentes respuestas, me gustaría señalar que no debe confundir return en el capítulo respecto a la Monad clase con el return palabra clave en lenguajes imperativos similares a C. Ellos son completamente diferente, pero mucha gente acostumbrada a esos idiomas ve return y no se da cuenta de que no está causando que se devuelva un valor, está haciendo un cálculo que devolverá ese valor cuando se evalúe en algún momento posterior. Al ser un lenguaje funcional, Haskell no necesita nada parecido a un return declaración. -

De hecho, la elección de Haskell de usar la palabra return fue, en mi humilde opinión, un gran error. Pero debido a la compatibilidad con versiones anteriores, ¡no hay vuelta atrás ahora! -

@DanBurton: siempre puedes crear un alias :) do_not_return a = return a -

Ya existe un alias que funciona con la mayoría de los tipos de Monad: pure. -

@JesseHallett "pure"También es un mal nombre. pure x implica tomar una x y hacerlo "puro" (como sin x toma x y devuelve el seno de x). pero en realidad es todo lo contrario: se necesita un "puro" x. -

6 Respuestas

Para ver por qué obtienes las respuestas particulares que surgen, las explicaciones de desazucarado son muy útiles. Permítanme complementarlos con un pequeño consejo general sobre el desarrollo de percepciones del código Haskell.

El sistema de tipos de Haskell no hace distinción entre dos propósitos "morales" separables:

  • [x] el tipo de valores que son listas con elementos extraídos de x
  • [x] el tipo de computaciones de elementos de x que permiten la elección prioritaria

El hecho de que estas dos nociones tengan la misma representación no significa que desempeñen los mismos roles. En f1, la [x, x+1] está desempeñando el papel de la computación, por lo que las posibilidades que genera se fusionan con la elección generada por toda la computación: eso es lo que el >>= de la lista que hace la mónada. En f2, sin embargo, el [x, x+1] está desempeñando el papel de valor, de modo que todo el cálculo genera una elección prioritaria entre dos valores (que resultan ser valores de lista).

Haskell no usa tipos para hacer esta distinción [y es posible que ya hayas adivinado que creo que debería hacerlo, pero esa es otra historia]. En su lugar, utiliza la sintaxis. Por lo tanto, debe entrenar su cabeza para percibir el valor y los roles de cálculo cuando lee el código. Él do La notación es una sintaxis especial para construir computaciones. lo que va dentro de la do se construye a partir del siguiente kit de plantilla:

piezas de rompecabezas para cálculos

Las tres piezas azules forman do-cálculos. He marcado los agujeros de cálculo en azul y los agujeros de valor en rojo. Esto no pretende ser una sintaxis completa, solo una guía sobre cómo percibir piezas de código en su mente.

De hecho, puede escribir cualquier expresión antigua en los lugares azules siempre que tenga un tipo monádico adecuado, y el cómputo así generado se fusionará con el cómputo general usando >>= según sea necesario. En tus f1 ejemplo, su lista está en un lugar azul y se trata como una opción prioritaria.

De manera similar, puede escribir expresiones en lugares rojos que muy bien pueden tener tipos monádicos (como listas en este caso), pero serán tratados como valores de todos modos. Eso es lo que pasa en f2: por así decirlo, los corchetes exteriores del resultado son azules, pero los corchetes interiores son rojos.

Entrene a su cerebro para hacer la separación valor/cómputo cuando lea el código, para que sepa instintivamente qué partes del texto están haciendo qué trabajo. Una vez que hayas reprogramado tu cabeza, la distinción entre f1 y f2 parecerá completamente normal!

Respondido 07 Jul 19, 16:07

Re "Haskell no usa tipos para hacer esta distinción [y es posible que ya haya adivinado que creo que debería...": Seguramente parte del punto del concepto de mónada es que estos tipos que tenemos por ahí de todos modos como lista, tal vez, etc., también sean mónadas porque admiten las operaciones correctas y, por lo tanto, pueden usarse como mónadas y tipos de contenedores. ¿Estás defendiendo que no hagamos de list una mónada, y en su lugar inventemos un nuevo tipo idéntico a list y lo hagamos una mónada, de modo que no podamos mezclar las operaciones normales de lista y las operaciones de mónada? - paquet

@Ben sí, eso parece ser lo que está defendiendo. hacemos newtype Ziplist a ..., ¿por qué la asimetría? Hay varios bind implementaciones posibles además de simples concatMap, digamos uno que alterna entre las corrientes, (`bind_i`f) = foldr (++/) [] . fmap f donde (x:xs) ++/ ys = x:ys ++/ xs. tendríamos que newtype eso también, así que ¿por qué la asimetría? Ese parece ser el argumento. Sospecho que en un nivel más profundo es una distinción entre lista como tipo concreto y como tipo abstracto. Podríamos implementar listas sobre árboles binarios equilibrados y tendríamos que replicar el código de definición de mónadas. - Will Ness

@WillNess Es cierto que, como máximo, una "versión" de la mónada de la lista se puede usar directamente en una lista nativa, por lo que existe una asimetría entre esa instancia "predeterminada" y las de tipo nuevo adicional. Podríamos evitar eso por siempre haciendo un nuevo tipo, pero esto no tiene nada que ver con las mónadas, por lo que probablemente deberíamos hacerlo para todas las clases de tipos, lo que me parece doloroso. - paquet

@WillNess ¿De qué manera parece ser eso lo que estoy defendiendo? Tuve mucho cuidado de no decir lo que estoy defendiendo. Tenga la seguridad de que no estoy defendiendo lo que sugiere que podría ser. El problema es cómo dividir un tipo como "permisos de efecto" y "tipo de valor". Haskell permite dos formas de hacer esto: en los lugares rojos, el único efecto es la parcialidad y la parte del valor es el tipo completo; en lugares azules, la parte de efecto se aplica a la parte de valor. La sintaxis de los términos selecciona qué análisis de tipos se utiliza. Preferiría ampliar la sintaxis de los tipos para marcar el límite de efecto/valor, pero de forma renegociable. - trabajador porcino

@WillNess No te preocupes. Mi lenguaje experimental, Frank, se acerca más a lo que tengo en mente. Le permite cambiar localmente la "mónada ambiental" en la que se interpreta la sintaxis aplicativa normal. No hay necesidad de return o elevación explícita de mónadas domesticadas a salvajes, pero hay thunking explícito (suspendiendo el cómputo como valor) y forzando (ejecutando cómputo suspendido). Es divertido, pero puede ser difícil de adaptar. - trabajador porcino

Las otras respuestas aquí son correctas, pero me pregunto si no son exactamente lo que necesita... Trataré de mantener esto lo más simple posible, solo dos puntos:


Punto 1. return no es una cosa especial en el lenguaje Haskell. No es una palabra clave, y no es azúcar sintáctico para otra cosa. Es solo una función que es parte del Monad clase de tipo. Su firma es simplemente:

return :: a -> m a

donde m es cualquier mónada de la que estemos hablando en ese momento. Toma un valor "puro" y lo mete en tu mónada. (Por cierto, hay otra función llamada pure eso es básicamente un sinónimo de return... ¡Me gusta más porque el nombre es más obvio!) De todos modos, si m es la mónada lista, entonces return tiene este tipo:

return :: a -> [a]

Si te ayuda, podrías pensar en el sinónimo de tipo type List a = [a], lo que podría hacer un poco más obvio que List es lo que estamos sustituyendo m. De todos modos, si fueras a implementar return usted mismo, la única forma razonable de implementarlo es tomando algún valor (de cualquier tipo a) y pegarlo en una lista por sí mismo:

return a = [a]

Entonces puedo decir return 1 en la lista mónada, y obtendré [1]. puedo decir igualmente return [1, 2, 3] y obtendré [[1, 2, 3]].


Punto 2. IO es una mónada, pero no todas las mónadas lo son IO. Muchos tutoriales de Haskell parecen fusionar los dos temas en gran parte por razones históricas (por cierto, las mismas razones históricas confusas que llevaron a return estar tan mal nombrado). Parece que podría tener cierta confusión (comprensible) al respecto.

En tu código, eres en la lista mónada porque escribiste do x <- [1, 2]. Si en cambio hubieras escrito do x <- getLine por ejemplo, estarías en el IO mónada (porque getLine devoluciones IO String). De todos modos, estás en la mónada de la lista, por lo que obtienes la definición de la lista de return descrito arriba. También obtienes la definición de la lista de >>=, que es solo (una versión invertida de) concatMap, definido como:

concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f xs = concat (map f xs)

Las otras respuestas publicadas prácticamente lo tienen cubierto desde aquí :) Sé que no respondí tu pregunta directamente, pero espero que estos dos puntos aborden las cosas fundamentales que podrías haber encontrado confusas.

Respondido 05 Jul 12, 05:07

Es fácil de ver cuando se vuelve a escribir el código con se unen y volvemos:

[1,2] >>= (\x->        [x,x+1]) === concatMap (\x-> [  x,x+1  ]) [1,2]

[1,2] >>= (\x-> return [x,x+1]) === concatMap (\x-> [ [x,x+1] ]) [1,2]

Tu primer código equivale a llamar join en los resultados de la segunda, eliminando una "capa" monádica introducida por return :: a -> m a, combinando "lista" de la mónada en uso, con el "lista" de tu valor Si estuvieras devolviendo un par, digamos, no habría tenido mucho sentido omitir el return:

                                     -- WRONG: type mismatch
[1,2] >>= (\x->        (x,x+1)) === concatMap (\x-> (  x,x+1  )) [1,2]
                                     -- OK:
[1,2] >>= (\x-> return (x,x+1)) === concatMap (\x-> [ (x,x+1) ]) [1,2]

O bien, podemos usar un join/fmap volver a escribir:

ma >>= famb === join (fmap famb ma)   -- famb :: a -> m b, m ~ []

join (fmap (\x->        [x,x+1]) [1,2]) = concat [ [  x,x+1  ] | x<-[1,2]]
join (fmap (\x->        (x,x+1)) [1,2]) = concat [ (  x,x+1  ) | x<-[1,2]]  -- WRONG
join (fmap (\x-> return [x,x+1]) [1,2]) = concat [ [ [x,x+1] ] | x<-[1,2]]

                                                         =  [y | x<-[1,2], y<-[ x,x+1 ]]
                                            {- WRONG -}  =  [y | x<-[1,2], y<-( x,x+1 )]
                                                         =  [y | x<-[1,2], y<-[[x,x+1]]]

Respondido 16 Feb 16, 10:02

el primer código, por leyes Mónadas, es equivalente a do {x <- [1,2]; r <- [x, x+1]; return r} que, con Monad Comprehensions, se escribe [ r | x <- [1,2], r <- [x,x+1] ]; el segundo es [ [x,x+1] | x <- [1,2] ]. entonces, f1 = flip ($) <$> [1,2] <*> [id,(1+)] y f2 = (\x->[x,x+1]) <$> [1,2]. -- con la tupla como en la respuesta obtenemos flip ($) <$> [1,2] <*> (id,(1+)) (Mal) y (\x->(x,x+1)) <$> [1,2] (OK). - Will Ness

El tipo de lista ([]) es una mónada, sí.

Ahora, recuerda lo que return hace. Esto es fácil de ver por su tipo de firma: return :: Monad m => a -> m a. Sustituyamos el tipo de lista en: return :: a -> [a]. Entonces, esta función toma algún valor y devuelve solo una lista de ese valor. es equivalente a \ x -> [x].

Entonces, en la primera muestra de código, tiene una lista al final: [x, x+1]. En la segunda muestra, usted tiene un anidado lista: una lista proviene de [x, x + 1] y una alternativa, la lista viene de return. La línea return [x, x + 1] podría ser reescrito para [[x, x + 1]] en este caso.

Al final, el resultado es la lista de todos los resultados posibles. Es decir, concatenamos el resultado de x as 1 y el resultado de x as 2 (Gracias a x <- [1,2] línea). Entonces, en el primer caso, concatenamos dos listas; en el segundo caso, concatenamos dos listas de listas, porque el extra return envolvió el resultado en una lista adicional.

Respondido 04 Jul 12, 11:07

Desazúcar el do sintaxis al equivalente

f1 = [1,2] >>= \x -> [x, x+1]
f2 = [1,2] >>= \x -> return [x, x+1]

Ahora, >>= proviene de la Monad clase,

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a

y el LHS de >>= en ambos f1 y f2 is [a] (dónde a ha incumplido Integer), por lo que realmente estamos considerando

instance Monad [] where
    (>>=) :: [a] -> (a -> [b]) -> [b]
    ...

Esto obedece a lo mismo leyes de la mónada como, pero es una mónada diferente de,

instance Monad IO where ...

y >>= para otras mónadas, así que no apliques ciegamente lo que sabes sobre una a la otra, ¿de acuerdo? :)

instance Monad [] se define así en GHC

instance Monad [] where
    m >>= k = foldr ((++) . k) [] m
    return x = [x]
    ...

pero es probablemente más fácil de entender []'s >>= as

instance Monad [] where
    m >>= k = concatMap k m

Si tomas esto y lo aplicas al original, obtienes

f1 = concatMap (\x -> [x, x+1]) [1,2]
f2 = concatMap (\x -> [[x, x+1]]) [1,2]

y está claro por qué los valores de f1 y f2 son lo que son.

Respondido 04 Jul 12, 07:07

Mi entendimiento es que hacer

devuelve [1,2] cuando en la mónada List es lo mismo que hacer

func :: Maybe (Maybe Int)
func = return $ Just 1

es por eso que terminas con listas envueltas, ya que [] son ​​solo azúcar sintáctica, ¿verdad?

cuando realmente quería hacer

func :: Maybe Int
func = return 5

or

func = Just 5

Creo que es un poco más fácil ver qué está pasando con la mónada Quizás.

Así que cuando lo hagas

return [1,2]

Estás haciendo lo mismo que

[ [1,2] ]

Respondido 09 Jul 12, 14:07

return Just 1 analiza como (return Just) 1 que no funciona :( - efímero

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.