Comprender las funciones puras y los efectos secundarios en Haskell - putStrLn

10

Recientemente, comencé a aprender Haskell porque quería ampliar mis conocimientos sobre programación funcional y debo decir que hasta ahora me encanta. El recurso que estoy usando actualmente es el curso 'Haskell Fundamentals Part 1' en Pluralsight. Desafortunadamente, tengo algunas dificultades para entender una cita particular del profesor sobre el siguiente código y esperaba que ustedes pudieran arrojar algo de luz sobre el tema.

Código de acompañamiento

helloWorld :: IO ()
helloWorld = putStrLn "Hello World"

main :: IO ()
main = do
    helloWorld
    helloWorld
    helloWorld

La frase

Si tiene la misma acción IO varias veces en un do-block, se ejecutará varias veces. Entonces, este programa imprime la cadena 'Hola Mundo' tres veces. Este ejemplo ayuda a ilustrar que putStrLnno es una función con efectos secundarios. Llamamos a la putStrLnfunción una vez para definir la helloWorldvariable. Si putStrLntuviera un efecto secundario de imprimir la cadena, solo se imprimiría una vez y la helloWorldvariable repetida en el bloque principal no tendría ningún efecto.

En la mayoría de los otros lenguajes de programación, un programa como este imprimiría 'Hello World' solo una vez, ya que la impresión ocurriría cuando putStrLnse llamara a la función. Esta sutil distinción a menudo hace tropezar a los principiantes, así que piense un poco en esto y asegúrese de comprender por qué este programa imprime 'Hello World' tres veces y por qué lo imprimiría solo una vez si la putStrLnfunción imprimiera como un efecto secundario.

Lo que no entiendo

Para mí parece casi natural que la cadena 'Hello World' se imprima tres veces. Percibo la helloWorldvariable (¿o función?) Como una especie de devolución de llamada que se invoca más tarde. Lo que no entiendo es cómo si putStrLntuviera un efecto secundario, la cadena se imprimiría solo una vez. O por qué solo se imprimiría una vez en otros lenguajes de programación.

Digamos que en el código C #, supongo que se vería así:

C # (violín)

using System;

public class Program
{
    public static void HelloWorld()
    {
        Console.WriteLine("Hello World");
    }

    public static void Main()
    {
        HelloWorld();
        HelloWorld();
        HelloWorld();
    }
}

Estoy seguro de que estoy pasando por alto algo bastante simple o malinterpreto su terminología. Cualquier ayuda sería muy apreciada.

EDITAR:

¡Gracias a todos por sus respuestas o comentarios! Sus respuestas me ayudaron a comprender mejor estos conceptos. No creo que haya hecho clic completamente todavía, pero volveré a visitar el tema en el futuro, ¡gracias!

Fluo
fuente
2
Piense en helloWorldser constante, como un campo o variable en C #. No hay ningún parámetro al que se esté aplicando helloWorld.
Caramiriel el
2
putStrLn no tiene un efecto secundario; simplemente devuelve una acción IO, la misma acción IO para el argumento, "Hello World"sin importar cuántas veces llame putStrLn.
chepner
1
Si lo hiciera, helloworldno sería una acción que imprima Hello world; sería el valor devuelto por putStrLn después de lo impreso Hello World(a saber, ()).
chepner
2
Creo que para entender este ejemplo, ya tendrías que entender cómo funcionan los efectos secundarios en Haskell. No es un buen ejemplo.
user253751
En tu fragmento de C # no te gusta helloWorld = Console.WriteLine("Hello World");. Simplemente contiene el Console.WriteLine("Hello World");en la HelloWorldfunción a ejecutar cada vez que HelloWorldse invoca. Ahora piensa en lo que helloWorld = putStrLn "Hello World"hace helloWorld. Se asigna a una mónada IO que contiene (). Una vez que lo vincula >>=, solo realizará su actividad (imprimiendo algo) y le dará ()en el lado derecho del operador de vinculación.
Reducido

Respuestas:

8

Probablemente sería más fácil entender lo que quiere decir el autor si definimos helloWorldcomo una variable local:

main :: IO ()
main = do
  let helloWorld = putStrLn "Hello World!"
  helloWorld
  helloWorld
  helloWorld

que puedes comparar con este pseudocódigo similar a C #:

void Main() {
  var helloWorld = {
    WriteLine("Hello World!")
  }
  helloWorld;
  helloWorld;
  helloWorld;
}

Es decir, en C # WriteLinees un procedimiento que imprime su argumento y no devuelve nada. En Haskell, putStrLnes una función que toma una cadena y le da una acción que imprimiría esa cadena si se ejecutara. Significa que no hay absolutamente ninguna diferencia entre escribir

do
  let hello = putStrLn "Hello World"
  hello
  hello

y

do
  putStrLn "Hello World"
  putStrLn "Hello World"

Dicho esto, en este ejemplo, la diferencia no es particularmente profunda, por lo que está bien si no entiendes a qué está tratando de llegar el autor en esta sección y simplemente continúas por ahora.

funciona un poco mejor si lo comparas con Python

hello_world = print('hello world')
hello_world
hello_world
hello_world

El punto aquí es que las acciones de E / S en Haskell son valores "reales" que no necesitan estar envueltos en "devoluciones de llamada" adicionales ni nada por el estilo para evitar que se ejecuten; más bien, la única forma de hacer que se ejecuten es para ponerlos en un lugar particular (es decir, en algún lugar dentro maino se generó un hilo main).

Esto tampoco es solo un truco de salón, sino que termina teniendo algunos efectos interesantes sobre cómo escribir código (por ejemplo, es parte de la razón por la cual Haskell realmente no necesita ninguna de las estructuras de control comunes con las que estaría familiarizado) con lenguajes imperativos y puede salirse con la suya haciendo todo en términos de funciones en su lugar), pero nuevamente no me preocuparía demasiado por esto (analogías como estas no siempre hacen clic inmediatamente)

Cúbico
fuente
4

Puede ser más fácil ver la diferencia como se describe si usa una función que realmente hace algo, en lugar de hacerlo helloWorld. Piense en lo siguiente:

add :: Int -> Int -> IO Int
add x y = do
  putStrLn ("I am adding " ++ show x ++ " and " ++ show y)
  return (x + y)

plus23 :: IO Int
plus23 = add 2 3

main :: IO ()
main = do
  _ <- plus23
  _ <- plus23
  _ <- plus23
  return ()

Esto imprimirá "Estoy agregando 2 y 3" 3 veces.

En C #, puede escribir lo siguiente:

using System;

public class Program
{
    public static int add(int x, int y)
    {
        Console.WriteLine("I am adding {0} and {1}", x, y);
        return x + y;
    }

    public static void Main()
    {
        int x;
        int plus23 = add(2, 3);
        x = plus23;
        x = plus23;
        x = plus23;
        return;
    }
}

Que se imprimiría solo una vez.

oisdk
fuente
3

Si la evaluación putStrLn "Hello World"tuviera efectos secundarios, entonces el mensaje solo se imprimiría una vez.

Podemos aproximar ese escenario con el siguiente código:

import System.IO.Unsafe (unsafePerformIO)
import Control.Exception (evaluate)

helloWorld :: ()
helloWorld = unsafePerformIO $ putStrLn "Hello World"

main :: IO ()
main = do
    evaluate helloWorld
    evaluate helloWorld
    evaluate helloWorld

unsafePerformIOtoma una IOacción y "olvida" que es una IOacción, desanimándola de la secuencia habitual impuesta por la composición de las IOacciones y dejando que el efecto tenga lugar (o no) de acuerdo con los caprichos de la evaluación perezosa.

evaluatetoma un valor puro y garantiza que el valor se evalúe siempre que se evalúe la IOacción resultante , lo que para nosotros será, porque se encuentra en el camino de main. Lo estamos usando aquí para conectar la evaluación de algunos valores a la ejecución del programa.

Este código solo imprime "Hello World" una vez. Tratamos helloWorldcomo un valor puro. Pero eso significa que se compartirá entre todas las evaluate helloWorldllamadas. ¿Y por qué no? Es un valor puro después de todo, ¿por qué volver a calcularlo innecesariamente? La primera evaluateacción "muestra" el efecto "oculto" y las acciones posteriores solo evalúan el resultado (), lo que no causa más efectos.

danidiaz
fuente
1
Vale la pena señalar que absolutamente no deberías estar usando unsafePerformIOen esta etapa de aprendizaje de Haskell. Tiene "inseguro" en el nombre por una razón, y no debe usarlo a menos que pueda (y lo haya hecho) considerar cuidadosamente las implicaciones de su uso en contexto. El código que danidiaz puso en la respuesta captura perfectamente el tipo de comportamiento poco intuitivo que puede resultar de unsafePerformIO.
Andrew Ray
1

Hay un detalle a tener en cuenta: llama a la putStrLnfunción solo una vez, mientras define helloWorld. En mainfunción, solo usa el valor de retorno de eso putStrLn "Hello, World"tres veces.

El profesor dice que la putStrLnllamada no tiene efectos secundarios y es cierto. Pero mire el tipo de helloWorld: es una acción de IO. putStrLnsolo lo crea para ti. Más tarde, encadena 3 de ellos con el dobloque para crear otra acción de E / S - main. Más tarde, cuando ejecutas tu programa, esa acción se ejecutará, ahí es donde se encuentran los efectos secundarios.

El mecanismo que se encuentra en la base de esto - mónadas . Este poderoso concepto le permite usar algunos efectos secundarios como imprimir en un lenguaje que no admite efectos secundarios directamente. Simplemente encadena algunas acciones, y esa cadena se ejecutará al inicio de su programa. Necesitará comprender ese concepto profundamente si quiere usar Haskell en serio.

Yuri Kovalenko
fuente