Posted on June 23, 2021

Haskell in C++: The IO Monad

In a previous post, I described how to implement Functors in C++. Now I will try to represent the IO monad, with a few toy examples.

§ What is the IO Monad

Disclaimer: This is my understanding of these concepts. There could be some technical inaccuracies, but the overall idea would still hold.

In very simple terms, f :: IO a is a computational recipe, which when executed produces a value of type a. In Haskell, we can construct a huge complex object of type IO () and call it main. Because you can only associate one monadic object with main, to do multiple input/output operations - one must somehow compose the different IO _ recipes (correctly) into a single master recipe.

To make things simpler and cleaner, I will use std::function to wrap and store the functions.

-- haskell
-- :k IO
-- IO :: * -> *

run :: IO a -> a
// c++
template <class A> struct IO {
  function<A()> recipe;
  IO(function<A()> r) : recipe(r) {}
  A run() { return recipe(); }
};

Note: The run function isn’t directly available in haskell, it’s just for showing the type. (Technically you can use unsafePerformIO, but it is not pure).

§ Functor, Applicative, Monad

First, I define the neccessary typeclass instances for IO.

fmap takes an IO action and a mapping function, and creates a new IO action - which when executed executes the input action, applies the mapping and returns the value.

instance Functor IO where
  fmap :: (a -> b) -> IO a -> IO b
template <typename A, typename B>
IO<B> fmap(function<B(A)> fn, IO<A> io_a) {
  return IO<B>([=]() { return fn(io_a.run()); });
}

Similarly I define the Applicative instance.

instance Applicative IO where
  pure :: a -> IO a
  -- I will call this `fappl` in c++
  (<*>) :: IO (a -> b) -> IO a -> IO b
template <typename A>
IO<A> pure(A val) {
  return IO<A>([=]() { return val; });
}

template <typename A, typename B>
IO<B> fappl(IO<function<B(A)>> io_a__b, IO<A> io_a) { // (<*>)
  return IO<B>([=]() { 
    function<B(A)> f = io_a__b.run();
    A a = io_a.run();
    return f(a);
  });
}

And finally Monad. As return is a keyword in c++, I will use Return instead. Remember that it is the same as pure.

instance Monad IO where
  return :: a -> IO a
  (>>=) :: IO a -> (a -> IO b) -> IO b
  (>>=) = bind
  (>>) :: IO a -> IO b -> IO b
  (>>) = seq
#define Return pure

// bind :: IO a -> (a -> IO b) -> IO b
template <typename A, typename B>
IO<B> bind(IO<A> io_a, function<IO<B>(A)> fn) {
  return IO<B>([=]() {
    A a = io_a.run();
    IO<B> io_b = fn(a);
    return io_b.run();
  });
}

// (>>=) = bind
template <typename A, typename B>
IO<B> operator>>=(IO<A> io_a, function<IO<B>(A)> fn) {
  return bind(io_a, fn);
}

// seq :: IO a -> IO b -> IO b
template <typename A, typename B>
IO<B> seq(IO<A> io_a, IO<B> io_b) {
  return IO<B>([=]() {
    io_a.run();
    return io_b.run();
  });
}

// (>>) = seq
template <typename A, typename B>
IO<B> operator>>(IO<A> io_a, IO<B> io_b) {
  return seq(io_a, io_b);
}

§ Some IO primitives

Now to define some IO primitives provided by System.IO.

First, the Unit type. There is a conceptual difference between the Void in Haskell and void in C++. In Hask (or in type theory), the Void type has no constructors, so a function returning Void is an impossible function!

Void in haskell is equivalent to something like llvm_unreachable. And () or the unit type in haskell is equivalent to void in c++. But to avoid this name confusion, I’ll explicitly use a Unit type.

// type () = ()
struct Unit {} unit;

Now, some output primitives:

// putChar :: Char -> IO ()
function<IO<Unit>(char)> putChar = [](char c) {
  return IO<Unit>([=]() {
    std::cout << c;
    return unit;
  });
};

// putStr :: String -> IO ()
function<IO<Unit>(string)> putStr = [](string s) {
  return IO<Unit>([=]() {
    std::cout << s;
    return unit;
  });
};

// putStrLn :: String -> IO ()
function<IO<Unit>(string)> putStrLn = [](string s) { return putStr(s + "\n"); };

And some input primitives:

// getChar :: IO Char
IO<char> getChar([]() {
  char c;
  std::cin >> c;
  return c;
});

// getLine :: IO String
IO<string> getLine([]() {
  string s;
  getline(std::cin, s);
  return s;
});

§ Finally, main!

We can now define a haskell-ey main function, right in c++!

main :: IO ()
main = putStrLn "Hello World!"
IO<Unit> Main = putStrLn("Hello World!");

// the actual c/c++ main. Run the hask `Main` function.
int main() { Main.run(); }

And that’s our hello world program, folks!

Also thanks to overloading >> and >>=, we can write very haskell-ey code!

main :: IO ()
main = putStr "Name: " >>
         fmap (\s -> "Hello " ++ s ++ "!") getLine
           >>= putStrLn
IO<Unit> Main =
  putStr("Name: ") >>
    fmap<string, string>([](string s) { return "Hello " + s + "!"; }, getLine)
      >>= putStrLn;

Note: I had to specify the template arguments for fmap because c++ could not deduce it.

Here is the full code used in the post. (preview on github gist)

§ Food for thought

Hope you enjoyed it!