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!

helloWorldser constante, como un campo o variable en C #. No hay ningún parámetro al que se esté aplicandohelloWorld.putStrLnno 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 llameputStrLn.helloworldno sería una acción que imprimaHello world; sería el valor devuelto porputStrLndespués de lo impresoHello World(a saber,()).helloWorld = Console.WriteLine("Hello World");. Simplemente contiene elConsole.WriteLine("Hello World");en laHelloWorldfunción a ejecutar cada vez queHelloWorldse invoca. Ahora piensa en lo quehelloWorld = putStrLn "Hello World"hacehelloWorld. 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.Respuestas:
Probablemente sería más fácil entender lo que quiere decir el autor si definimos
helloWorldcomo una variable local:que puedes comparar con este pseudocódigo similar a C #:
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 escribiry
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
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 hilomain).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)
fuente
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:Esto imprimirá "Estoy agregando 2 y 3" 3 veces.
En C #, puede escribir lo siguiente:
Que se imprimiría solo una vez.
fuente
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:
unsafePerformIOtoma unaIOacción y "olvida" que es unaIOacción, desanimándola de la secuencia habitual impuesta por la composición de lasIOacciones 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 laIOacción resultante , lo que para nosotros será, porque se encuentra en el camino demain. 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 lasevaluate helloWorldllamadas. ¿Y por qué no? Es un valor puro después de todo, ¿por qué volver a calcularlo innecesariamente? La primeraevaluateacción "muestra" el efecto "oculto" y las acciones posteriores solo evalúan el resultado(), lo que no causa más efectos.fuente
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 deunsafePerformIO.Hay un detalle a tener en cuenta: llama a la
putStrLnfunción solo una vez, mientras definehelloWorld. Enmainfunción, solo usa el valor de retorno de esoputStrLn "Hello, World"tres veces.El profesor dice que la
putStrLnllamada no tiene efectos secundarios y es cierto. Pero mire el tipo dehelloWorld: es una acción de IO.putStrLnsolo lo crea para ti. Más tarde, encadena 3 de ellos con eldobloque 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.
fuente