I think an "operational monad" approach is better for explaining the essence of Haskell I/O. The Haskell version can be quite simple:
data PrimOp a where
PutStr :: String -> PrimOp ()
GetLine :: PrimOp String
-- Whatever other primitives you want
data MyIO a where
Pure :: a -> MyIO a
Bind :: !(MyIO a) -> (a -> MyIO b) -> MyIO b
LiftPrim :: !(PrimOp a) -> MyIO a
instance Functor MyIO where
fmap = liftM
instance Applicative MyIO where
pure = Pure
(<*>) = ap
instance Monad MyIO where
(>>=) = Bind
The MyIO
values aren't some magical world-passing functions; they're just plain data. We can interpret the data to actually perform the action represented if we so desire:
runPrimOp :: PrimOp a -> IO a
runPrimOp (PutStr s) = putStr s
runPrimOp GetLine = getLine
runMyIO :: MyIO a -> IO a
runMyIO (Pure a) = pure a
runMyIO (Bind m f) = runMyIO m >>= runMyIO . f
runMyIO (LiftPrim prim) = runPrimOp prim
It would actually be possible to write a Haskell compiler whose IO
type looks a lot like MyIO
, and whose runtime system interprets values of that type directly.
I have attempted a translation to Java below. I've never really been a Java programmer, and it's been a long time since I used it at all, so this may be terribly unidiomatic or even wrong. I imagine you'd probably want to use some version of the "visitor pattern" to represent interpretations of Bind
and Pure
(bringing things into the realm of something general like Control.Monad.Operational
), while using a run
method for a PrimOp
subclass of IO
. Since I don't really know the right Java way I've tried to just keep it simple.
public interface IO <A> {
public A run ();
}
public final class Pure <A> implements IO <A> {
private final A val;
Pure (A x) { val = x; }
public A run () {
return val;
}
}
The tricky bit is Bind
, which needs existential quantification. I don't know the idiomatic way to do this in Java, so I did something awkward that seems to work. Namely, I wrote an auxiliary class OpenBind
that exposes two type variables, and then a class Bind
that wraps up an OpenBind
leaving one of those variables wild.
import java.util.function.Function;
public final class Bind <A> implements IO <A> {
private final OpenBind <?,A> ob;
public <B> Bind (IO <B> m, Function <B,IO <A>> f) {
ob = new OpenBind <B,A> (m, f);
}
public A run() {
return (ob.run());
}
private final static class OpenBind <Fst,Snd> {
private final IO <Fst> start;
private final Function <Fst, IO <Snd>> cont;
public OpenBind (IO <Fst> m, Function <Fst, IO <Snd>> f) {
start = m;
cont = f;
}
public final Snd run () {
Fst x = start.run();
IO <Snd> c = cont.apply(x);
return (c.run());
}
}
}
The primops themselves are pretty simple (I couldn't find a Java equivalent of ()
so I wrote my own Unit
):
public class PutStr implements IO <Unit> {
private String str;
public PutStr (String s) {
str = s;
}
public Unit run () {
System.out.print(str);
return Unit.unit;
}
}
public final class Unit {
private Unit () {}
public static final Unit unit = new Unit ();
}
public class GetLine implements IO <String> {
private GetLine () {}
public static final GetLine getLine = new GetLine ();
public String run () {
// Replace the next line with whatever you actually use to
// read a string.
return "";
}
}