When discussing pure programming, there’s two “tricky terms” that often get confused: side effects and effects.
These terms aren’t tricky because they’re hard to grasp or not understandable, but because they are often misused, or given different (sometimes conflicting) meanings.
Quite often the Humpty-Dumpty principle of definitions is used to explain them.
I’ll try my best define them as with the least amount of hand waving here.
In most of article I’ll restrict myself to a subset of pure programming - functional programming. Functional is programming is programming with functions 1 2.
Functions are “constructs” (relations) that map the domain (we often refer to it as input) into the co-domain (output) so that there’s exactly one such mapping for each input.
In other words a function cannot return two different values for the same inputs, or more formally:
\[\forall x_1, x_2 \in X \quad x_1 = x_2 \Rightarrow f(x_1) = f(x_2)\]where \(X\) is the domain, and \(f(x)\) is in the co-domain.
The term equational reasoning is a property that functional programs exhibit.
In context of functional programming equational reasoning means that a functional program can always (this is important) be understood as a series of substitution steps. In other word we may always substitute a function call with it’s output.
Equational reasoning is a consequence of functional programming, it’s not a property which can be given or taken away from functional programming.
A side effect is everything that breaks equational reasoning.
It follows that there’s no such thing as a side effecting function, because a function can only have one mapping from the domain to the co-domain and side-effects introduce an additional “non-functional” element.
So, if side-effects are not possible, how do we read files, print to screen and launch rockets in a functional program?
Let’s imagine a function that returns an integer:
Now, imagine that the result of this function were interpreted by a special interpreter which printed that value out to screen (or launched \(n\) number of rockets, etc.).
It’s hard to say that a function that returns an Int
is somehow side
effecting, even when run by the imaginary interpreter above.
Believe it or not, that’s the entire secret about “printing to screen” in a functional setting - it’s not really about “special values,” but a question of “special interpreters”.
To give a layman definition of an effect, I’d like to propose this: an effect is a value that needs a special interpreter (usually in the form of a compiler library) that, when interpreted, will directly affect state, values, or the “physical world”.
In Haskell there’s an IO
type which, while not special in itself, has a
special status in the Haskell compiler-library - it’s the result of the main
function which has an IO
type which is “later” interpreted, and the interpreter
will print to the screen, read files, launch rockets, etc.
The main
function in Haskell is also nothing special. It’s just an convention
for the compiler-library for the name of the main entry point of a program -
similarly to C’s int main(char**)
procedure or Java’s public static void
main(String[])
static method.
Because the IO type is a value like any other, a function can return it, manipulate it, drop it, etc. It has no special meaning outside of the interpreter run by invisibly at the end of our Haskell program.
While the Scala library and the compiler doesn’t throw us a bone here, with some
diligence we, can also track IO effects in Scala using scalaz.effect
To beat the point in, there’s nothing special about the program
function or
its output value. It’s just an IO type.
Unfortunately, there’s also nothing special about Scala’s ability to facilitate
safe programming, so we have to provide our own interpreter to interpret this
value. We do this by calling unsafePerformIO
on the IO
type.
Fortunately, if we call unsafePerformIO
exactly once at the “last possible
moment” this is relatively safe and though we cheat, we won’t harm equational
reasoning this way.
Scalaz also provides an SafeAppt trait which can perform the above step for you.
There’s also nothing special about monads itself. While I glossed over this fact
previously, both the IO
type in Haskell and the IO
type provided by Scalaz
are monads.
It’s a frequent misunderstanding, but the fact that the IO types are monads is really unimportant, and it’s just implementation detail.
To contrast this I’ll show how IO looks in Mercury:
The operational semantics of Mercury programs (which have more common with logic programs than functional programs) are so alien for the functional programmer I’ll try to use functional analogues to explaining how the above works.
The main
predicate is the analogue of the main
function in Haskell.
We will read the predicate main(io::di, io::uo) is det
as
“deterministic” predicate main with destructive input variable of the type io,
and unique output variable of the type io
as a function io -> io
.
There’s additional trickery here that ensures that after “reading” from the input variable once, we cannot “read” from it again. We get this trickery from Mercury’s type system which allows uniqueness typing (similar types are available in Clean).
This “trickery” guarantees that the io
values are built in sequence and the same
guarantees are ensured by making the IO
type a monad.
The above program can be understood as a series of substitutions (in logical programs, this process is called unification) and could be described as:
IO_1
to the io
type which when interpreted will print the string “Hello”IO_2
to the io
type which when interpreted will print the string “Hello “IO_3
to the io
type which when interpreted will print the string “Hello world!”IO_Out
to the io
type which when interpreted will print the string “Hello world!\n”This is a contrived example but shows how at the basics, controlling effects isn’t anything specific to a monad.