Necesita una explicación de la sintaxis básica del bloque do

Aug 19 2020

En ghci, escribí:

 let x = do
    i <- [1..5]
    j <- [2..4]
    return i 

Resultado Esperado:

[1,2,3,4,5]

Resultado actual:

[1,1,1,2,2,2,3,3,3,4,4,4,5,5,5]

No entiendo la lógica detrás de esa salida. Creo que la razón podría ser algo relacionado con la mónada, pero soy muy nuevo en la programación funcional, desearía que alguien pudiera explicarlo un poco.

También probé la forma equivalente en Comprensión de listas y el resultado es el mismo, lo que significa que hay algo básico que no entendí aquí.

Respuestas

5 leftaroundabout Aug 19 2020 at 13:08

También probé la forma equivalente en comprensión de listas y el resultado es el mismo

Buena idea. Sucede que para las listas, la donotación hace exactamente lo mismo que las listas por comprensión. (De hecho, hay una extensión sintáctica que le permite usar la notación de comprensión de listas para cualquier mónada, como puede usar la donotación para cualquier mónada).

Entonces, estás preguntando por qué [a | a<-[0,1], b<-[2,3]]da en [0,0,1,1]lugar de [0,1]. La forma en que esto parece sorprendente es si piensa en listas por comprensión como comprensiones establecidas como las que encontrará en matemáticas. Pero las listas no son conjuntos, aunque los Haskellers a menudo usan listas como un sustituto improvisado de conjuntos. Si las listas de comprensión actuaron como comprensión de conjuntos, entonces

  [x | x <- [0,1,0]]

también debería rendir solo [0,1]como resultado (o al menos, debería producir el mismo resultado que lo [x|x<-[0,1]]hace).

En general, este tipo de eliminación de duplicados requiere verificaciones de igualdad y, si desea que sea eficiente, también un método de ordenación o hash. Las listas no hacen tal cosa, por lo que si desea un comportamiento similar al de un conjunto, debe usar una estructura de datos de implementación de conjuntos. Sety HashSetson los más comunes.

6 jpmarinier Aug 19 2020 at 12:54

Esto se debe a que al mecanismo do no le importa (afortunadamente) si el código más interno realmente se refiere o no a (algunas de) las variables del ciclo.

Mira, siempre obtienes 3 * 5 = 15 valores independientemente del código más interno:

 λ> 
 λ> xs1 = do { i <- [1..5] ; j <- [2..4] ; return i }
 λ> xs1
[1,1,1,2,2,2,3,3,3,4,4,4,5,5,5]
 λ> 
 λ> xs2 = do { i <- [1..5] ; j <- [2..4] ; return 9 }
 λ> xs2
[9,9,9,9,9,9,9,9,9,9,9,9,9,9,9]
 λ> 
 λ> xs3 = do { i <- [1..5] ; j <- [2..4] ; return (i,j) }
 λ> xs3
[(1,2),(1,3),(1,4),(2,2),(2,3),(2,4),(3,2),(3,3),(3,4),(4,2),(4,3),(4,4),(5,2),(5,3),(5,4)]
 λ> 
 λ> length xs1
15
 λ> length xs2
15
 λ> length xs3
15
 λ> 

Por lo que puedo decir, este es un comportamiento perfectamente estándar, que Haskell comparte con C, C ++, Fortran, Python ...

Un ejemplo equivalente a C ++:

#include  <vector>
#include  <iostream>

int main()
{
    std::vector<int>  vi{1,2,3,4,5};
    std::vector<int>  vj{2,3,4};

    for (int i: vi)
        for (int j: vj)
            std::cout << i << ", ";

    std::cout << std::endl;

    return EXIT_SUCCESS;
}

Salida de C ++:

$ ./a.out 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, $