Entendendo funções puras e efeitos colaterais em Haskell - putStrLn

10

Recentemente, comecei a aprender Haskell porque queria ampliar meu conhecimento em programação funcional e devo dizer que estou realmente amando isso até agora. O recurso que estou usando atualmente é o curso 'Haskell Fundamentals Part 1' sobre Pluralsight. Infelizmente, tenho dificuldade em entender uma citação específica do palestrante sobre o código a seguir e esperava que vocês pudessem lançar alguma luz sobre o tópico.

Código de acompanhamento

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

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

A citação

Se você tiver a mesma ação de E / S várias vezes em um bloco bloqueado, ela será executada várias vezes. Portanto, este programa imprime a string 'Hello World' três vezes. Este exemplo ajuda a ilustrar que putStrLnnão é uma função com efeitos colaterais. Chamamos a putStrLnfunção uma vez para definir a helloWorldvariável. Se putStrLntivesse um efeito colateral de imprimir a sequência, ela seria impressa apenas uma vez e a helloWorldvariável repetida no bloco principal não teria nenhum efeito.

Na maioria das outras linguagens de programação, um programa como esse imprime 'Hello World' apenas uma vez, pois a impressão ocorre quando a putStrLnfunção é chamada. Essa distinção sutil costuma atrapalhar os iniciantes, então pense um pouco sobre isso e compreenda por que esse programa imprime 'Hello World' três vezes e por que putStrLno imprimia apenas uma vez se a função fazia a impressão como efeito colateral.

O que eu não entendo

Para mim, parece quase natural que a string 'Hello World' seja impressa três vezes. Eu percebo a helloWorldvariável (ou função?) Como um tipo de retorno de chamada que é chamado mais tarde. O que eu não entendo é como, se putStrLntivesse um efeito colateral, a string seria impressa apenas uma vez. Ou por que seria impresso apenas uma vez em outras linguagens de programação.

Digamos que no código C #, eu presumo que ficaria assim:

C # (violino)

using System;

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

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

Tenho certeza de que estou ignorando algo bastante simples ou interpretando mal sua terminologia. Qualquer ajuda seria muito apreciada.

EDITAR:

Obrigado a todos por suas respostas! Suas respostas me ajudaram a entender melhor esses conceitos. Acho que ainda não clicou completamente, mas revisarei o tópico no futuro, obrigado!

Fluous
fonte
2
Pense em helloWorldser constante, como um campo ou variável em C #. Não há nenhum parâmetro que esteja sendo aplicado helloWorld.
Caramiriel # 25/19
2
putStrLn não tem efeito colateral; simplesmente retorna uma ação de E / S, a mesma ação de E / S para o argumento, "Hello World"não importa quantas vezes você chame putStrLn.
chepner
11
Se tivesse, helloworldnão seria uma ação que imprime Hello world; seria o valor retornado putStrLn após a impressão Hello World(ou seja, ()).
chepner
2
Eu acho que para entender esse exemplo, você já teria que entender como os efeitos colaterais funcionam em Haskell. Não é um bom exemplo.
user253751
No seu snippet C # você não gosta helloWorld = Console.WriteLine("Hello World");. Você só conter o Console.WriteLine("Hello World");na HelloWorldtoda função a ser executada HelloWorldé invocado. Agora pense sobre o que helloWorld = putStrLn "Hello World"faz helloWorld. Ele é atribuído a uma mônada de E / S que contém (). Depois de vinculado, >>=ele somente executará sua atividade (imprimindo alguma coisa) e fornecerá ()o lado direito do operador de vinculação.
Redu

Respostas:

8

Provavelmente seria mais fácil entender o que o autor quer dizer se definirmos helloWorldcomo uma variável local:

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

que você pode comparar com esse pseudocódigo do tipo C #:

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

Ou seja, em C # WriteLineé um procedimento que imprime seu argumento e não retorna nada. No Haskell, putStrLné uma função que pega uma string e fornece uma ação que imprimiria essa string, caso fosse executada. Isso significa que não há absolutamente nenhuma diferença entre escrever

do
  let hello = putStrLn "Hello World"
  hello
  hello

e

do
  putStrLn "Hello World"
  putStrLn "Hello World"

Dito isto, neste exemplo a diferença não é particularmente profunda, por isso é bom se você não entender o que o autor está tentando obter nesta seção e seguir em frente por enquanto.

funciona um pouco melhor se você comparar com python

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

O ponto aqui é que as ações de IO no Haskell são valores "reais" que não precisam ser agrupados em "retornos de chamada" adicionais ou qualquer coisa do tipo para impedir sua execução. Em vez disso, a única maneira de fazê- los executar é para colocá-los em um local específico (ou seja, em algum lugar dentro mainou um fio gerado main).

Isso também não é apenas um truque de salão, acaba tendo alguns efeitos interessantes sobre como você escreve código (por exemplo, é parte do motivo pelo qual Haskell realmente não precisa de nenhuma das estruturas de controle comuns que você conheceria com linguagens imperativas e pode fazer tudo em termos de funções), mas novamente não me preocuparia muito com isso (analogias como essas nem sempre clicam imediatamente)

Cúbico
fonte
4

Pode ser mais fácil ver a diferença conforme descrito se você usar uma função que realmente faz alguma coisa, em vez de helloWorld. Pense no seguinte:

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 ()

Isso exibirá "Estou adicionando 2 e 3" 3 vezes.

Em C #, você pode escrever o seguinte:

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;
    }
}

O que seria impresso apenas uma vez.

oisdk
fonte
3

Se a avaliação putStrLn "Hello World"tiver efeitos colaterais, a mensagem será impressa apenas uma vez.

Podemos aproximar esse cenário com o seguinte 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 uma IOação e "esquece" é uma IOação, desviando-a do seqüenciamento habitual imposto pela composição das IOações e permitindo que o efeito ocorra (ou não) de acordo com os caprichos da avaliação preguiçosa.

evaluateassume um valor puro e garante que o valor seja avaliado sempre que a IOação resultante for avaliada - o que para nós será, porque está no caminho de main. Estamos usando aqui para conectar a avaliação de alguns valores à exeção do programa.

Este código imprime apenas "Hello World" uma vez. Tratamos helloWorldcomo um valor puro. Mas isso significa que será compartilhado entre todas as evaluate helloWorldchamadas. E porque não? Afinal, é um valor puro, por que recalculá-lo desnecessariamente? A primeira evaluateação "exibe" o efeito "oculto" e as ações posteriores apenas avaliam o resultado (), o que não causa mais efeitos.

danidiaz
fonte
11
Vale a pena notar que você absolutamente não deve estar usando unsafePerformIOnesta fase do aprendizado de Haskell. Ele possui um nome "inseguro" por um motivo e você não deve usá-lo, a menos que possa (e tenha) considerado cuidadosamente as implicações de seu uso no contexto. O código que danidiaz colocou na resposta captura perfeitamente o tipo de comportamento não intuitivo que pode resultar unsafePerformIO.
Andrew Ray
1

Há um detalhe a ser observado: você chama a putStrLnfunção apenas uma vez, enquanto define helloWorld. Na mainfunção, você apenas usa o valor de retorno disso putStrLn "Hello, World"três vezes.

O palestrante diz que a putStrLnligação não tem efeitos colaterais e é verdade. Mas observe o tipo de helloWorld- é uma ação de IO. putStrLnapenas cria para você. Mais tarde, você encadeia 3 deles com o dobloco para criar outra ação de IO - main. Mais tarde, quando você executar seu programa, essa ação será executada, é aí que estão os efeitos colaterais.

O mecanismo que está na base disso - as mônadas . Esse conceito poderoso permite que você use alguns efeitos colaterais, como imprimir em um idioma que não suporta diretamente efeitos colaterais. Você apenas encadeia algumas ações, e essa cadeia será executada no início do seu programa. Você precisará entender profundamente esse conceito se quiser usar o Haskell a sério.

Yuri Kovalenko
fonte