How can we do inherently side-effecty things like IO when using functional programming, where each call has to be side-effect free?
In this post we’ll look at side-effects and how we could eliminate them from our code (we’ll use C#, but we can apply the idea to many languages). The aim is not to get something enormously practical that we’ll use every day, but instead to explore some ideas and hopefully work out how it is possible to get any useful work done using functional programming where side-effects are forbidden.
Side-effects
A function’s output depends purely on its input. For example, +
is a function because its output will be based entirely on the numbers given as input. If we call it with the same input, we’ll always get the same output.
What about the inc()
method of a mutable counter? It is not a function, because if we call inc()
once we get a counter set to 1
, but if we call inc()
again we get a counter set to 2
.
In other words inc()
has a side-effect; it does something other than produce an output from its input. This has some downsides, so we’d like to avoid it where possible. But how can we avoid side-effects for something like Console.WriteLine()
, where we mutate the state of the console with each call?
Haskell does not let us express side-effects at all1 but still manages to do IO, so it must be possible. Can we get the same effect (hah!) in C#?
Simple IO
Let’s have a look at a simple C# program that reads some input and writes to the console:
The SideEffecty
method has side-effects all over the place. It doesn’t have any output (it is void
), so we can hardly say that its output depends purely on its input, and even if it did have output, it doesn’t take any inputs anyway!
How can we make this side-effect free? By cheating: we simply don’t execute the offending bit of code.
We can now execute NotSoSideEffecty()
without any side-effects at all. We can consider NotSoSideEffecty()
to be a function that takes no input (also known as Unit
or ()
in languages like F#, Scala and Haskell), and always returns a program that will prompt for some input and write some output.2 We can call NotSoSideEffecty()
as often as we like; as far as we’re concerned it will always return the same output for its (empty) input.
If we defer all side-effects by wrapping them up in Action
(for things like writing to console) and Func<T>
(for things like reading input), we can assemble side-effect free programs that do absolutely nothing until we execute the composed program in our Main
method.3
Slightly inconvenient
I think we could actually get fairly far with this approach, but it is not exactly convenient. We have to be very careful about invoking any delegates lest they actually represent a side-effect, unless we remember to wrap that invocation within an Action
too.
What we need is another way of packaging up these IO actions.
An IO
type
Rather than using delegate types, let’s create an IO<T>
type to represent a program that will produce some value of type T
. An example is ReadLine()
, which when run will produce a string
it reads from the console. We’ll use a placeholder Unit
type for programs that return void
, like WriteLine(string s)
which produces a program that prints some output but does not return any value.
Just as before, we are packaging up our side-effects rather than running them immediately. We’re not going to be accidentally executing these any time soon though – we haven’t provided any way to run them at all!
We’ve also got a static IO
class with a few built-in IO programs; WriteLine()
and ReadLine()
. But how do we put these together to build a program that works the same as NotSoSideEffecty()
?
Combining IO
programs
The first thing NotSoSideEffecty()
does is prompt for input, then read a line of input. We’d like to do this by combining our IO.WriteLine(string s)
and IO.ReadLine()
programs. We’ll call this combining function Then
4:
The next thing we need to do is construct a new program that takes the value that will be produced from ReadLine()
and writes it to console. If we had access to the string from ReadLine()
we could just write a function that takes string
and returns an IO<Unit>
, but we can’t get that string without running our programming and performing a side-effect. We’ll have to carefully put this together in the IO<T>
class:
Now assuming we sneak in a way of running an IO action, we can replicate our NotSoSideEffecty()
program:
LINQing it up
We can make our side-effect-free code easier to follow by using LINQ. To play nicely with LINQ we need to provide a SelectMany
function with the right signature. This will be very similar to our second Then
function, but it will have to take an additional selector
that combines the values from the first and second IO programs:
And now we can use LINQ comprehension syntax to clean up a little:
a
and b
variables are required to play nicely with the LINQ syntax. They will be set to the result of running the IO.WriteLine()
programs, which will just be Unit
.
A quick Haskell IO diversion
Although I’m probably over-simplifying things, this is how I tend to think about Haskell IO. Haskell has a type IO a
which wraps up side-effects, and we have several pure functions available to help us combine them. Haskell’s main
function returns an IO ()
(IO unit) so the entire program is completely side-effect free. The Haskell execution engine then takes care of invoking that program.5
To see the similarities, here’s the equivalent program in Haskell:
Reading and writing to Console ain’t that exciting
We’re not limited to ReadLine
and WriteLine
– we can wrap any side-effect up in an IO action. We could return IO actions that iterate over files, generate random numbers, make network calls etc.
While we may not want to do our C# IO like this, it could be a useful technique for combining things like async actions or database operations using pure functions, before running them from impure, side-effecting parts of our code.
Conclusion
One way to eliminate side-effects is to not execute side-effecting code. This sounds more than a little like cheating, but it seems just crazy enough to work! :) Instead of running side-effects, we can return values which represent programs with side-effects.
We can use functions like Then()
and SelectMany()
(and others we didn’t look at in this post) to combine these values into larger programs, all without causing a side-effect.
For this post we used this approach to perform very basic IO, but the same idea works for system, file system, network, and database calls.
There’s a whole lot I need to learn before I can start using these concepts6, but I think I’m starting to get an idea of how functional programming can be used to construct programs that do useful work, without us having to resort to side-effects.
If you’ve tried pushing side-effect free functions fairly far into your code in languages that generally promote side-effects everywhere, I’d love to hear how it’s worked out for you.
With the exception of a few cheats↩
Another way of thinking about this is as
NotSoSideEffecty()
returning an effect as a value, rather than a side-effect.↩We could also write a shim that takes a DLL with an
Action Main()
function and executes that, so our entire program is pure.↩Then
is a valid function: its output depends purely on its input.↩Haskell’s IO is actually implemented as a state transformer (ST), which is more like
Func<World, Tuple<World,T>>
than theFunc<T>
we used, but I think these end up being fairly comparable in the case of IO. See Lazy Functional State Threads (1994) by John Launchbury and Simon Peyton Jones for more on this topic.↩Specifically, I need to learn more about iteratee-style structures to effectively use resources with pure FP, and probably some monad transformer stuff.↩