Search code examples
functional-programmingpurely-functional

How to perform side-effects in pure functional programming?


I am dealing with the concept of functional programming for a while now and find it quite interesting, fascinating and exciting. Especially the idea of pure functions is awesome, in various terms.

But there is one thing I do not get: How to deal with side-effects when restricting yourself to pure functions.

E.g., if I want to calculate the sum of two numbers, I can write a pure function (in JavaScript):

var add = function (first, second) {
  return first + second;
};

No problem at all. But what if I want to print the result to the console? The task of "printing something to the console" is not pure by definition - but how could / should I deal with this in a pure functional programming language?


Solution

  • There are a few approaches to this. One thing you will just have to accept is that at some point, there exists a magical impure machine that takes pure expressions and makes them impure by interacting with the environment. You are not supposed to ask questions about this magical machine.

    There are two approaches I can think of off the top of my head. There exists at least a third one I have forgotten about.


    I/O Streams

    The approach that is easiest to understand could be streaming I/O. Your main function takes one argument: a stream of things that have happened on the system – this includes keypresses, files on the file system, and so on. Your main function also returns one thing: a stream of things that you want to happen on the system.

    Streams are like lists, mind you, only you can build them one element at a time and the recipient will receive the element as soon as you have built it. Your pure program reads from such a stream, and appends to its own stream when it wants the system to do something.

    The glue that makes all of this work is a magical machine that sits outside of your program, reads from the "request" stream and puts stuff into the "answers" stream. While your program is pure, this magical machine is not.

    The output stream could look like this:

    [print('Hello, world! What is your name?'), input(), create_file('G:\testfile'), create_file('C:\testfile'), write_file(filehandle, 'John')]
    

    and the corresponding input stream would be

    ['John', IOException('There is no drive G:, could not create file!'), filehandle]
    

    See how the input in the out-stream resulted in 'John' appearing in the in-stream? That's the principle.

    Monadic I/O

    Monadic I/O is what Haskell does, and does really well. You can imagine this as building a giant tree of I/O commands with operators to glue them together, and then your main function returns this massive expression to a magical machine that sits outside of your program and executes the commands and performs the operations indicated. This magical machine is impure, while your expression-building program is pure.

    You might want to imagine this command tree looking something like

    main
      |
      +---- Cmd_Print('Hello, world! What is your name?')
      +---- Cmd_WriteFile
               |
               +---- Cmd_Input
               |
               +---+ return validHandle(IOResult_attempt, IOResult_safe)
                   + Cmd_StoreResult Cmd_CreateFile('G:\testfile') IOResult_attempt
                   + Cmd_StoreResult Cmd_CreateFile('C:\testfile') IOResult_safe
    

    The first thing it does is print a greeting. The next thing it does is that it wants to write a file. To be able to write to the file, it first needs to read from the input whatever it's supposed to write to the file. Then it is supposed to have a file handle to write to. It gets this from a function called validHandle that returns the valid handle of two alternatives. This way, you can mix what looks like impure code with what looks like pure code.


    This "explanation" is bordering on asking questions about the magical machine you're not supposed to ask questions about, so I'm going to wrap this up with a few pieces of wisdom.

    • Real monadic I/O looks nowhere near my example here. My example is one of the possible explanations for how monadic I/O can look like "under the hood" without breaking purity.

    • Do not try to use my examples to understand how to work with pure I/O. How something works under the hood is something completely different to how you do things with it. If you had never seen a car before in your life, you wouldn't become a good driver by reading the blueprints for one either.

      The reason I keep saying you're not supposed to ask questions about the magical machine that actually does stuff is that when programmers learn things, they tend to want to go poke at the machinery to try to figure it out. I don't recommend doing so for pure I/O. The machinery might not teach you anything about how to use different variants of I/O.

      This is similar to how you don't learn Java by looking at the disassembled JVM bytecode.

    • Do learn to use monadic I/O and stream-based I/O. It's a cool experience and it's always good to have more tools under your toolbelt.