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 putStrLn
não é uma função com efeitos colaterais. Chamamos a putStrLn
função uma vez para definir a helloWorld
variável. Se putStrLn
tivesse um efeito colateral de imprimir a sequência, ela seria impressa apenas uma vez e a helloWorld
variá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 putStrLn
funçã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 putStrLn
o 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 helloWorld
variável (ou função?) Como um tipo de retorno de chamada que é chamado mais tarde. O que eu não entendo é como, se putStrLn
tivesse 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!
helloWorld
ser constante, como um campo ou variável em C #. Não há nenhum parâmetro que esteja sendo aplicadohelloWorld
.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ê chameputStrLn
.helloworld
não seria uma ação que imprimeHello world
; seria o valor retornadoputStrLn
após a impressãoHello World
(ou seja,()
).helloWorld = Console.WriteLine("Hello World");
. Você só conter oConsole.WriteLine("Hello World");
naHelloWorld
toda função a ser executadaHelloWorld
é invocado. Agora pense sobre o quehelloWorld = putStrLn "Hello World"
fazhelloWorld
. 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.Respostas:
Provavelmente seria mais fácil entender o que o autor quer dizer se definirmos
helloWorld
como uma variável local:que você pode comparar com esse pseudocódigo do tipo C #:
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 escrevere
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
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
main
ou um fio geradomain
).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)
fonte
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:Isso exibirá "Estou adicionando 2 e 3" 3 vezes.
Em C #, você pode escrever o seguinte:
O que seria impresso apenas uma vez.
fonte
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:
unsafePerformIO
toma umaIO
ação e "esquece" é umaIO
ação, desviando-a do seqüenciamento habitual imposto pela composição dasIO
ações e permitindo que o efeito ocorra (ou não) de acordo com os caprichos da avaliação preguiçosa.evaluate
assume um valor puro e garante que o valor seja avaliado sempre que aIO
ação resultante for avaliada - o que para nós será, porque está no caminho demain
. 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
helloWorld
como um valor puro. Mas isso significa que será compartilhado entre todas asevaluate helloWorld
chamadas. E porque não? Afinal, é um valor puro, por que recalculá-lo desnecessariamente? A primeiraevaluate
ação "exibe" o efeito "oculto" e as ações posteriores apenas avaliam o resultado()
, o que não causa mais efeitos.fonte
unsafePerformIO
nesta 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 resultarunsafePerformIO
.Há um detalhe a ser observado: você chama a
putStrLn
função apenas uma vez, enquanto definehelloWorld
. Namain
função, você apenas usa o valor de retorno dissoputStrLn "Hello, World"
três vezes.O palestrante diz que a
putStrLn
ligação não tem efeitos colaterais e é verdade. Mas observe o tipo dehelloWorld
- é uma ação de IO.putStrLn
apenas cria para você. Mais tarde, você encadeia 3 deles com odo
bloco 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.
fonte