Usar return vs. no usar return en la mónada de lista
Frecuentes
Visto 6,426 equipos
40
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]]
6 Respuestas
45
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 dex
[x]
el tipo de computaciones de elementos dex
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:
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
30
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
24
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
14
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
8
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
1
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 list haskell monads do-notation or haz tu propia pregunta.
Además de estas excelentes respuestas, me gustaría señalar que no debe confundir
return
en el capítulo respecto a laMonad
clase con elreturn
palabra clave en lenguajes imperativos similares a C. Ellos son completamente diferente, pero mucha gente acostumbrada a esos idiomas vereturn
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 unreturn
declaración. - Matthew WaltonDe 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! - Dan Burton@DanBurton: siempre puedes crear un alias :)
do_not_return a = return a
- Jakub M.Ya existe un alias que funciona con la mayoría de los tipos de Monad:
pure
. - Jesse Hallett@JesseHallett "
pure
"También es un mal nombre.pure x
implica tomar unax
y hacerlo "puro" (comosin x
tomax
y devuelve el seno dex
). pero en realidad es todo lo contrario: se necesita un "puro"x
. - Will Ness