Pure Functions and I/O

The goal of this lesson is to answer the question, “Because pure functions can’t have I/O, how can an FP application possibly get anything done if all of its functions are pure functions?”

Given my pure function mantra, “Output depends only on input,” a perfectly rational question at this point is:

“How do I get anything done if I can’t read any inputs or write any outputs?”

Great question!

The answer is that you violate the “Write Only Pure Functions” rule! It seems like other books go through great lengths to avoid answering that question until the final chapters, but I just gave you that answer fairly early in this book. (You’re welcome.)

The general idea is that you write as much of your application as possible in an FP style, and then handle the UI and all forms of input/output (I/O) (such as Database I/O, Web Service I/O, File I/O, etc.) in the best way possible for your current programming language and tools.

In Scala the percentage of your code that’s considered impure I/O will vary, depending on the application type, but will probably be in this range:

  • On the low end, it will be about the same as a language like Java. So if you were to write an application in Java and 20% of it was going to be impure I/O code and 80% of it would be other stuff, in FP that “other stuff” will be pure functions. This assumes that you treat your UI, File I/O, Database I/O, Web Services I/O, and any other conceivable I/O the same way that you would in Java, without trying to “wrap” that I/O code in “functional wrappers.” (More on this shortly.)

  • On the high end, it will approach 100%, where that percentage relies on two things. First, you wrap all of your I/O code in functional wrappers. Second, your definition of “pure function” is looser than the definition I have stated thus far.

I don’t mean to make a joke or be facetious in that second statement. It’s just that some people may try to tell you that by putting a wrapper layer around I/O code, the impure I/O function somehow becomes pure. Maybe somewhere in some mathematical sense that is correct, I don’t know. Personally, I don’t buy that.

Let me explain what I’m referring to.

Imagine that in Scala you have a function that looks like this:

def promptUserForUsername: String = ???

Clearly this function is intended to reach out into the outside world and prompt a user for a username. You can’t tell how it does that, but the function name and the fact that it returns a String gives us that impression.

Now, as you might expect, every user of an application (like Facebook or Twitter) should have a unique username. Therefore, any time this function is called, it will return a different result. By stating that (a) the function gets input from a user, and (b) it can return a different result every time it’s called, this is clearly not a pure function. It is impure.

However, now imagine that this same function returns a String that is wrapped in another class that I’ll name IO:

def promptUserForUsername: IO[String] = ???

This feels a little like using the Option/Some/None pattern in Scala.

That’s interesting, but what does this do for us?

Personally, I think it has one main benefit: I can glance at this function signature, and know that it deals with I/O, and therefore it’s an impure function. In this particular example I can also infer that from the function name, but what if the function was named differently?:

def getUsername: IO[String] = ???

In this case getUsername is a little more ambiguous, so if it just returned String, I wouldn’t know exactly how it got that String. But when I see that a String is wrapped with IO, I know that this function interacts with the outside world to get that String. That’s pretty cool.

But this is where it gets interesting: some people state that wrapping promptUserForUsername’s return type with IO makes it a pure function.

I am not that person.

The way I look at it, the first version of promptUserForUsername returned String values like these:

"alvin"
"kim"
"xena"

and now the second version of promptUserForUsername returns that same infinite number of different strings, but they’re wrapped in the IO type:

IO("alvin")
IO("kim")
IO("xena")

Does that somehow make promptUserForUsername a pure function? I sure don’t think so. It still interacts with the outside world, and it can still return a different value every time it’s called, so by definition it’s still an impure function.

As Martin Odersky states in this Google Groups Scala debate:

“The IO monad does not make a function pure. It just makes it obvious that it’s impure.”

As I noted in the “What is This Lambda You Speak Of?” chapter, monads were invented in 1991, and added to Haskell in 1998, with the IO monad becoming Haskell’s way of handling input/output. Therefore, I’d like to take a few moments to explain why this is such a good idea in Haskell.

If you come from the Java world, the best thing you can do at this moment is to forget anything you know of how the Java Virtual Machine (JVM) works. By that, I mean that you should not attempt to apply anything you know about the JVM to what I’m about to write, because the JVM and Haskell compiler are as different as dogs and cats.

Haskell is considered a “pure” functional programming language, and when monads were invented in the 1990s, the IO monad became the Haskell way to handle I/O. In Haskell, any function that deals with I/O must declare its return type to be IO. This is not optional. Functions that deal with I/O must return the IO type, and this is enforced by the compiler.

For example, imagine that you want to write a function to read a user’s name from the command line. In Haskell you’d declare your function signature to look like this:

getUsername :: IO String

In Scala, the equivalent function will have this signature:

def getUsername: IO[String] = ???

A great thing about Haskell is that declaring that a function returns something inside of an outer “wrapper” type of IO is a signal to the compiler that this function is going to interact with the outside world. As I’ve learned through experience, this is also a nice signal to other developers who need to read your function signatures, indicating, “This function deals with I/O.”

There are two consequences of the IO type being a signal to the Haskell compiler:

  1. The Haskell compiler is free to optimize any code that does not return something of type IO. This topic really requires a long discussion, but in short, the Haskell compiler is free to re-order all non-IO code in order to optimize it. Because pure functional code is like algebra, the compiler can treat all non-IO functions as mathematical equations. This is somewhat similar to how a relational database optimizes your queries. (That is a very short summary of a large, complicated topic. I discuss this more in the “Functional Programming is Like Algebra” lesson.)

  2. You can only use Haskell functions that return an IO type in certain areas of your code, specifically (a) in the main block or (b) in a do block. Because of this, if you attempt to use the getUsername function outside of a main or do block, your code won’t compile.

If that sounds pretty hardcore, well, it is. But there are several benefits of this approach.

First, you can always tell from a function’s return type whether it interacts with the outside world. Any time you see that a function returns something like an IO[String], you know that String is a result of an interaction with the outside world. Similarly, if the type is IO[Unit], you can be pretty sure that it wrote something to the outside world. (Note that I wrote those types using Scala syntax, not Haskell syntax.)

Second, when you’re working on a large programming team, you know that a stressed-out programmer under duress can’t accidentally slip an I/O function into a place where it shouldn’t be.

You know how it is: a deadline is coming up and the pressure is intense. Then one day someone on the programming team cracks and gives in to the pressure. Rather than doing something “the right way,” he does something expedient, like accessing a database directly from a GUI method. “I’ll fix it later,” he rationalizes as he incurs Technical Debt. But as we know, later never comes, and the duct tape stays there until that day when you’re getting ready to go on vacation and it all falls apart.

I’ll explore this topic more in the I/O lessons in this book, but at this point I want to show that there is a very different way of thinking about I/O than what you might be used to in languages like C, C++, Java, C#, etc.

As I showed in this lesson, when you need to write I/O code in functional programming languages, the solution is to violate the “Only Write Pure Functions” rule. The general idea is that you write as much of your application as possible in an FP style, and then handle the UI, Database I/O, Web Service I/O, and File I/O in the best way possible for your current programming language and tools.

I also showed that wrapping your I/O functions in an IO type doesn’t make a function pure, but it is a great way to add something to your function’s type signature to let every know, “This function deals with I/O.” When a function returns a type like IO[String] you can be very sure that it reached into the outside world to get that String, and when it returns IO[Unit], you can be sure that it wrote something to the outside world.

So far I’ve covered a lot of background material about pure functions, and in the next lesson I share something that was an important discovery for me: The signatures of pure functions are much more meaningful than the signatures of impure functions.

  • The this Google Groups Scala debate where Martin Odersky states, “The IO monad does not make a function pure. It just makes it obvious that it’s impure.”

results matching ""

    No results matching ""