Reputation: 2515
Recently, I started learning Haskell because I wanted to broaden my knowledge on functional programming and I must say I am really loving it so far. The resource I am currently using is the course 'Haskell Fundamentals Part 1' on Pluralsight. Unfortunately I have some difficulty understanding one particular quote of the lecturer about the following code and was hoping you guys could shed some light on the topic.
Accompanying Code
helloWorld :: IO ()
helloWorld = putStrLn "Hello World"
main :: IO ()
main = do
helloWorld
helloWorld
helloWorld
The Quote
If you have the same IO action multiple times in a do-block, it will be run multiple times. So this program prints out the string 'Hello World' three times. This example helps illustrate that putStrLn
is not a function with side effects. We call the putStrLn
function once to define the helloWorld
variable. If putStrLn
had a side effect of printing the string, it would only print once and the helloWorld
variable repeated in the main do-block wouldn't have any effect.
In most other programming languages, a program like this would print 'Hello World' only once, since the printing would happen when the putStrLn
function was called. This subtle distinction would often trip up beginners, so think about this a bit, and make sure you understand why this program prints 'Hello World' three times and why it would print it only once if the putStrLn
function did the printing as a side effect.
What I don't understand
For me it seems almost natural that the string 'Hello World' is printed three times. I perceive the helloWorld
variable (or function?) as a sort of callback which is invoked later. What I don't understand is, how if putStrLn
had a side effect, it would result in the string being printed only once. Or why it would only be printed once in other programming languages.
Let's say in C# code, I would presume it would look like this:
C# (Fiddle)
using System;
public class Program
{
public static void HelloWorld()
{
Console.WriteLine("Hello World");
}
public static void Main()
{
HelloWorld();
HelloWorld();
HelloWorld();
}
}
I am sure I am overlooking something quite simple or misinterpret his terminology. Any help would be greatly appreciated.
EDIT:
Thank you all for your answers! Your answers helped me get a better understanding of these concepts. I don't think it fully clicked yet, but I will revisit the topic in the future, thank you!
Upvotes: 12
Views: 984
Reputation: 27766
If the evaluation of putStrLn "Hello World"
had side effects, then then message would only be printed once.
We can approximate that scenario with the following code:
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
unsafePerformIO
takes an IO
action and "forgets" it's an IO
action, unmooring it from the usual sequencing imposed by the composition of IO
actions and letting the effect take place (or not) according to the vagaries of lazy evaluation.
evaluate
takes a pure value and ensures that the value is evaluated whenever the resulting IO
action is evaluated—which for us it will be, because it lies in the path of main
. We are using it here to connect the evaluation of some values to the exection of the program.
This code only prints "Hello World" one time. We treat helloWorld
as a pure value. But that means it will be shared between all evaluate helloWorld
calls. And why not? It's a pure value after all, why recalculate it needlessly? The first evaluate
action "pops" the "hidden" effect and the later actions just evaluate the resulting ()
, which doesn't cause any further effects.
Upvotes: 3
Reputation: 10091
It might be easier to see the difference as described if you use a function that actually does something, rather than helloWorld
. Think of the following:
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 ()
This will print out "I am adding 2 and 3" 3 times.
In C#, you might write the following:
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;
}
}
Which would print only once.
Upvotes: 5
Reputation: 1375
There is one detail to notice: you call putStrLn
function only once, while defining helloWorld
. In main
function you just use the return value of that putStrLn "Hello, World"
three times.
The lecturer says that putStrLn
call has no side effects and it's true. But look at the type of helloWorld
- it is an IO action. putStrLn
just creates it for you. Later, you chain 3 of them with the do
block to create another IO action - main
. Later, when you execute your program, that action will be run, that's where side effects lie.
The mechanism that lies in base of this - monads. This powerful concept allows you to use some side effects like printing in a language that doesn't support side effects directly. You just chain some actions, and that chain will be run on start of your program. You will need to understand that concept deeply if you want to use Haskell seriously.
Upvotes: 1
Reputation: 15683
It’d probably be easier to understand what the author means if we define helloWorld
as a local variable:
main :: IO ()
main = do
let helloWorld = putStrLn "Hello World!"
helloWorld
helloWorld
helloWorld
which you could compare to this C#-like pseudocode:
void Main() {
var helloWorld = {
WriteLine("Hello World!")
}
helloWorld;
helloWorld;
helloWorld;
}
I.e. in C# WriteLine
is a procedure that prints its argument and returns nothing. In Haskell, putStrLn
is a function that takes a string and gives you an action that would print that string were it to be executed. It means that there is absolutely no difference between writing
do
let hello = putStrLn "Hello World"
hello
hello
and
do
putStrLn "Hello World"
putStrLn "Hello World"
That being said, in this example the difference isn’t particularly profound, so it’s fine if you don’t quite get what the author is trying to get at in this section and just move on for now.
it works a bit better if you compare it to python
hello_world = print('hello world')
hello_world
hello_world
hello_world
The point here being that IO actions in Haskell are “real” values that don’t need to be wrapped in further “callbacks” or anything of the sort to prevent them from executing - rather, the only way to do get them to execute is to put them in a particular place (i.e. somewhere inside main
or a thread spawned off main
).
This isn’t just a parlour trick either, this does end up having some interesting effects on how you write code (for example, it’s part of the reason why Haskell doesn’t really need any of the common control structures you’d be familiar with from imperative languages and can get away with doing everything in terms of functions instead), but again I wouldn’t worry too much about this (analogies like these don’t always immediately click)
Upvotes: 10