Recently to a friend, I quipped that it’d be a good exercise towards demystifying Haskell’s IO type to write a comparable IO type in your favorite language. In this blog post, I do that for Java, Javascript, Python, and Scala.

TL;DR: Here’s the code.

## Understanding Functional APIs

My favorite way of understanding a type is by looking at its constructors, combinators, and eliminators. But what do I mean by constructors, combinators, and eliminators? Let me show you by looking at a more familiar type first before we tackle IO.

We call Set an opaque type because its authors chose to hide its data constructors. Instead of letting us muck around with the internals, they give us some functions that we can use to create sets.

Notice that each of those functions returns a set, and none of them require a set in any of its inputs, so we call these functions constructors.

There are also a few functions for manipulating sets.

Each function in this group takes one or more sets in its input and returns a set. These functions are thought of as combining their inputs in some way to create new sets, so we call them combinators.

Finally, we need a way to get usable information out of a set. The authors provide us with a few functions that take sets as input but don’t mention Set in their output, so we call them eliminators.

Any functional API for a type can be understood by listing out its constructors, combinators, and eliminators, and the dreaded IO type is no different.

## API of the IO Type

So, what are the constructors, combinators, and eliminators of the IO type?

Like Set, IO is opaque. To find IO’s constructors, we notice that IO is an instance of the Monad type class, which provides the return function:

What are the other constructors? Well, Prelude is full of them:

At first glance, these don’t feel like constructors, but trust me for now.

We get combinators from the Monad and Functor type classes:

These functions take one or more IOs in their input and produce an IO in their output, so they’re perfectly reasonable combinators.

One thing you might notice, though, is a curious lack of eliminators. In fact, Haskell has a single designated way of eliminating an IO value: naming it main.

Naming your IO value main and running your program is how you eliminate the IO type and get access to the useful information inside.

The crucial thing about Haskell’s IO type is that it’s referentially transparent, which means that a value of type IO a doesn’t perform any action, it only describes an action. putStrLn "Hello!" doesn’t print to the screen: it describes printing to the screen. getLine doesn’t get a line of input, it describes the action of getting a line of input. You could call this the Command Pattern if that helps (but don’t dwell on it if it doesn’t).

This is why it’s reasonable to call putStrLn and getLine constructors: they allow you to construct IO values that describe some actions, and then you can further modify those action using the combinators fmap and bind ((>>=)). As you write your program, you are describing the actions that should take place, and the only action that ultimately does take place is the one you named main.

## A Simple Program

I wrote this simple program in order to have something to port over to the other languages. It uses the IO constructors, combinators, and eliminator we’ve listed above ((>>=) and (>>) are used implicitly in do notation), so we’ll have to port those to the other languages as well.

In each language, I port the IO type and its API in one module and I port the app in another module. This is to mimic practical usage, where you’ll want the IO type to be in its own library.

## Java

First, the IO library.

Like in Haskell, Java’s IO is an opaque type. We accomplish this by making the class constructor and sole instance field private. We provide a few static factory methods as part of the public API.

Combinators are instance methods.

A single eliminator, with an ominous name, rounds out the API.

Now, the app. Unlike the library, the app is not very pretty, but it does demonstrate a few key points.

We’re able to maintain a clean separation between core logic and the tedium of talking to the outside.

The IO type is extensible, in the sense that we may define our own custom IO operations. We’re not limited to those found in the IO library.

We maintain referential transparency by waiting until main to use unsafeRunIO. The result is that app is a first-class value. We are free to reuse it anywhere, or inline it, or otherwise refactor as we see fit.

## Javascript

The IO library in Javascript is delightfully small.

The outer IO refers to the name of the module. The inner IO refers to the name of the class. We’re able to hide the class by only exporting our three constructors.

We need an HTML file to run our Javascript app in the browser. It’s in this file that we call unsafeRunIO.

Finally, our Javascript port of our app.

## Python

Python is a delightful language that seems to revel in side effects (mostly in the form of assignment statements). Let’s see if we can encapsulate IO here.

Hopefully, the IO library looks second-nature by now. Remember, the object of the game is to create a data structure that describes the effects to be done, but doesn’t do them until unsafeRunIO is called.

The only tricky part in Python is that lambdas can’t span multiple lines. It’s easy enough to get around this by defining and using a named function, though.

## Scala

The Scala IO library is even shorter than the Javascript one. We make the IO type opaque by marking it as sealed and by making the companion apply method private.

Since Scala supports call-by-name arguments, we have a very convenient syntax IO(...) for “promoting” side-effectful code blocks into first-class actions.

In Scala we benefit from having for comprehensions, so our port follows the original Haskell much more closely (though the port is not quite as pretty as the original).

Thanks for reading. I hope you enjoyed this post, and I sincerely hope that I helped eliminate some of the mysticism for you. It’s not magic, after all.