Skip to main content

Explicit effects

The original motivation for explicit effects

  • Given lazy evaluation as a strategy, the moment of evaluation is not easy to predict and hence not a good trigger for side-effecting actions.
  • Even worse, it may be difficult to predict whether a term is evaluated at all.
  • We would like to keep equational reasoning, and allow compiler optimisations such as
    • strictness analysis – evaluating things earlier than needed if they will definitely be needed, or
    • speculative evaluation – evaluating things even if they might not be needed at all.

The classic approach

In most languages, execution of side effects is tied to evaluation of the side-effecting expression.

This is feasible for languages with eager evaluation, because the order in which expressions are written down corresponds closely to the resulting order of evaluation.

With lazy evaluation, this is not the case ...

Problematic programs

Assume for the time being:

getLine :: String

Consider:

program1 =
let
x = getLine
y = getLine
in
x ++ y
program2 =
let
x = getLine
in
x ++ x
program3 =
let
x = getLine
y = getLine
in
y ++ x

If evaluation triggers the effect and evaluation is lazy, then when and how far we look at the resulting string will determine if and when lines are being read.

Using equational reasoning, all three programs should mean the same.

The Haskell approach

We do not tie the execution of side effects to evaluation.

We introduce a new datatype IO and make evaluation and execution separate concepts!

Evaluation vs. execution

data IO a -- abstract

The type of plans to perform effects that ultimately yield an a .

  • Evaluation does not trigger the actual effects. It will at most evaluate the plan.
  • Execution triggers the actual effects. Executing a plan is not possible from within a Haskell program.

The main program

main :: IO ()
  • The entry point into the program is a plan to perform effects (a possibly rather complex one).
  • This is the one and only plan that actually gets executed.

The unit type

data () = () -- special syntax

Constructor:

() :: ()
  • A type with a single value (nullary tuple).
  • Often used to parameterize other types.
  • A plan for actions with no interesting result: IO ().

Execution of effects via GHCi

For convenience, GHCi also executes IO actions:

GHCi> getLine
Some text.
"Some text."
getLine :: IO String

A plan that when executed, reads a line interactively and returns that line as a String.

Execution of effects with unit results in GHCi

GHCi does not print the final result of IO () -typed actions:

GHCi> writeFile "test.txt" "Hello"
GHCi> putStrLn "two\nlines"
two
lines

writeFile :: FilePath -> String -> IO ()
putStrLn :: String -> IO ()

Explicit effects are a good idea

(Not just in Haskell, not just in a lazily evaluated language.)

  • We can see via the type of a program whether it is guaranteed to have no side effects, or whether it is allowed to use effects.
  • In principle, we can even make more fine-grained statements than just yes or no, by allowing just specific classes of effects.
  • Encourages a programming style that keeps as much as possible effect-free.
  • Makes it easier to test programs, or to run them in a different context.