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.

data Set a

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

empty     ::                 Set a
singleton ::            a -> Set a
fromList  :: Ord a => [a] -> Set a

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.

insert       :: Ord a =>     a -> Set a -> Set a
delete       :: Ord a =>     a -> Set a -> Set a
union        :: Ord a => Set a -> Set a -> Set a
intersection :: Ord a => Set a -> Set a -> Set a

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.

null       ::                   Set a -> Bool
member     :: Ord a =>     a -> Set a -> Bool
size       ::                   Set a ->  Int
isSubsetOf :: Ord a => Set a -> Set a -> Bool

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?

data IO a

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:

return :: a -> IO a

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

putStrLn :: String ->     IO ()
getLine  ::           IO String
...

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

We get combinators from the Monad and Functor type classes:

fmap  :: (a -> b) ->        IO a -> IO b
(>>=) ::     IO a -> (a -> IO b) -> IO b
(>>)  ::     IO a ->        IO b -> IO b
...

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.

main :: IO ()

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.

-- App.hs
appLogic :: String -> String -> String
appLogic x y = "Result: " <> show (read x + read y)

printMaybe :: Maybe String -> IO ()
printMaybe = maybe (return ()) putStrLn

prompt :: Maybe String -> (String -> Maybe String) -> IO String
prompt greet confirm = do
  printMaybe greet
  l <- getLine
  printMaybe (confirm l)
  return l

app :: IO ()
app = do
  x <- prompt
    (Just "Please input two numbers.")
    (Just . ("Got first input: " <>))
  y <- prompt
    Nothing
    (Just . ("Got second input: " <>))
  putStrLn (appLogic x y)

main :: IO ()
main = app

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.

// IO.java
import java.util.function.Function;
import java.util.function.Supplier;

public final class IO<A> {
  private final Supplier<A> run;

  private IO(Supplier<A> run) {
    this.run = run;
  }
// ...

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.

// ...

  /** Constructors */

  public static IO<String> getLine = new IO<String>( () ->
    System.console().readLine() );

  public static IO<Void> putStrLn(String str) {
    return new IO<Void>( () -> {
      System.out.println(str);
      return null;
    });
  }

  public static <A> IO<A> _return(A x) {
    return new IO<A>( () -> x );
  }
// ...

Combinators are instance methods.

// ...

  /** Combinators */

  public <B> IO<B> map(Function<A, B> f) {
    return new IO<B>( () ->
      f.apply(this.unsafeRunIO()) );
  }

  public <B> IO<B> bind(Function<A, IO<B>> f) {
    return new IO<B>( () ->
      f.apply(this.unsafeRunIO()).unsafeRunIO() );
  }

  public <B> IO<B> and(IO<B> other) {
    return new IO<B>( () -> {
      this.unsafeRunIO();
      return other.unsafeRunIO();
    });
  }
// ...

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

// ...

  /** Eliminators */

  public A unsafeRunIO() {
    return run.get();
  }
}

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

// App.java
import java.util.function.Function;
import java.util.Optional;

public final class App {

// ...

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

// ...

  private static String appLogic(String x, String y) {
    return "Result: " + Integer.toString(
      Integer.parseInt(x) + Integer.parseInt(y));
  }
// ...

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.

// ...

  private static IO<Void> printMaybe(Optional<String> x) {
    return x.map(IO::putStrLn).orElse(IO._return(null));
  }

  private static IO<String> prompt( Optional<String> greet,
                                    Function<String, Optional<String>> confirm ) {
    return printMaybe(greet)
      .and(IO.getLine).bind( l ->
        printMaybe(confirm.apply(l))
          .and(IO._return(l)));
  }
// ...

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.

// ...

  public static IO<Void> app =
    prompt( Optional.of("Please input two numbers."),
            l -> Optional.of("Got first input: " + l) ).bind( x ->
    prompt( Optional.empty(),
            l -> Optional.of("Got second input: " + l) ).bind( y ->
    IO.putStrLn(appLogic(x, y)) ));

  public static void main(String[] args) {
    app.unsafeRunIO();
  }
}

Javascript

The IO library in Javascript is delightfully small.

// IO.js
const IO = (function() {
  'use strict';

  const IO = (run) => {
    return {
      unsafeRunIO: run,

      map: (f) => IO(() => f(run())),

      bind: (f) => IO(() => f(run()).unsafeRunIO()),

      and: (x) => IO(() => {
        run();
        return x.unsafeRunIO();
      })
    };
  };

  return {
    getLine: IO(() => window.prompt("")),

    putStrLn: (str) => IO(() => {
      window.alert(str);
      return null;
    }),

    return: (x) => IO(() => x)
  };
}());

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.

<!-- App.html -->
<html>
  <head>
    <script type="text/javascript" src="./IO.js"></script>
    <script type="text/javascript" src="./App.js"></script>
  </head>
  <body>
    <script type="text/javascript">
      App.main.unsafeRunIO()
    </script>
  </body>
</html>

Finally, our Javascript port of our app.

// App.js
const App = (function() {
  'use strict';

  const appLogic = (x, y) => 'Result: ' + (parseInt(x) + parseInt(y));

  const printMaybe = (strM) => strM == null ? IO.return(null) : IO.putStrLn(strM)

  const prompt = (greet, confirm) =>
    printMaybe(greet)
      .and(IO.getLine).bind( l =>
        printMaybe(confirm(l))
          .and(IO.return(l)));

  const app =
    prompt( 'Please input two numbers.',
            l => 'Got first input: ' + l ).bind( x =>
    prompt( null,
            l => 'Got second input: ' + l ).bind( y =>
    IO.putStrLn(appLogic(x, y)) ));

  return {
    main: 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.

# IO.py
class IO:

  def __init__(self, run):
    self.unsafeRunIO = run

  def map(self, f):
    return IO(lambda: f(self.unsafeRunIO()))

  def bind(self, f):
    return IO(lambda: f(self.unsafeRunIO()).unsafeRunIO())

  def _and(self, other):
    def go():
      self.unsafeRunIO()
      return other.unsafeRunIO()
    return IO(go)

def _return(x):
  return IO(lambda: x)

def putStrLn(str):
  def go():
    print(str)
    return None
  return IO(go)

getLine = IO(input)

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.

# App.py
import IO

def appLogic(x, y):
  return "Result: " + str(int(x) + int(y))

def printMaybe(strM):
  return IO.putStrLn(strM) if strM is not None else IO._return(None)

def prompt(greet, confirm):
  salute = printMaybe(greet)._and(IO.getLine)
  certify = lambda l: printMaybe(confirm(l))._and(IO._return(l))
  return salute.bind(certify)

app = prompt( "Please input two numbers.",
              lambda l: "Got first input: " + str(l) ).bind(lambda x:
      prompt( None,
              lambda l: "Got second input: " + str(l) ).bind(lambda y:
      IO.putStrLn(appLogic(x, y)) ))

if __name__ == "__main__":
  app.unsafeRunIO()

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.

// IO.scala
sealed trait IO[A] {

  def unsafeRunIO: A

  final def map[B](f: A => B): IO[B] = IO(f(unsafeRunIO))

  final def flatMap[B](f: A => IO[B]): IO[B] = IO(f(unsafeRunIO).unsafeRunIO)

  final def and[B](x: IO[B]): IO[B] = IO {
    unsafeRunIO
    x.unsafeRunIO
  }
}

object IO {

  private def apply[A](x: => A): IO[A] = new IO[A] {
    def unsafeRunIO: A = x
  }

  val getLine: IO[String] = IO(io.StdIn.readLine)

  def putStrLn(str: String): IO[Unit] = IO(println(str))

  def _return[A](x: A): IO[A] = IO(x)
}

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).

// App.scala
object App {

  def appLogic(x: String, y: String): String =
    "Result: " + (x.toInt + y.toInt).toString

  def printMaybe(x: Option[String]): IO[Unit] =
    x.fold(IO._return(()))(IO.putStrLn)

  def prompt( greet: Option[String],
              confirm: String => Option[String]
            ): IO[String] = for {
    _ <- printMaybe(greet)
    l <- IO.getLine
    _ <- printMaybe(confirm(l))
  } yield l

  val app: IO[Unit] = for {
    x <- prompt( Some("Please input two numbers."),
                 l => Some("Got first input: " + l) )
    y <- prompt( None,
                 l => Some("Got second input: " + l) )
    _ <- IO.putStrLn(appLogic(x, y))
  } yield ()

  def main(args: Array[String]) = app.unsafeRunIO
}

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.