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.
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).
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);
}
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;
});
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)
Main.run
, and how the IO
monad construction actually works.Hope you enjoyed it!