Fluous
Fluous

Reputation: 2515

Understanding pure functions and side effects in Haskell - putStrLn

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

Answers (4)

danidiaz
danidiaz

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

oisdk
oisdk

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

Yuri Kovalenko
Yuri Kovalenko

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

Cubic
Cubic

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

Related Questions