Recently to a friend, I quipped that it’d be a good exercise towards demystifying Haskell’s
IO type to write a comparable
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
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 is opaque. To find
IO’s constructors, we notice that
IO is an instance of the
Monad type class, which provides the
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
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 designated way of eliminating
IO: naming your function
Naming your function
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
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 does take place is the one you named
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 (
(>>) 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.
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.
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
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.
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.
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 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.
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.