UP | HOME

The power of OOP and abstraction

The beginning is known equation: IO a = State RealWorld a1. This makes me think: what if RealWorld can be inherited? It turns out I build IO monad in Java like below:

static class Ability implements FileSystem, Console {}
static class Ability2 implements Console {}

static IO<One> step1() {
        return IO.<Ability>mk()
                .bind((w) -> {
                    var content = w.readFile(Path.of("test.txt"));
                    w.println("file content: " + content);
                    return IO.pure(One.mk());
                })
                .with(new Ability2())
                .bind((w) -> {
                    w.println("hello world");
                    return IO.pure(One.mk());
                });
    }

// invoke the IO monad
step1().run(new Ability());

完美的用法:使用者可以任意更改 ability set 的 IO Monad pic.twitter.com/8xUpTLiV5w

— 悃悃 (@dannypsnl) April 30, 2023

1. But we are not in functional world

We have no do notation or macro in Java, and that turns out is good, that made me rethink about how to use such idea realistic. First, we already have mutable in Java by default, we don't have to using State monad at all, instead, we can just update the RealWorld object in IO class! Second, we don't actually need to combine the effect by a monad, we can just do it. Therefore, the correct question would be: how could we limit the computation?

2. Inherited computation

Hence, I get the solution: we only use inherited computation in our interface! For example, we have:

public interface Example extends FileSystem, Console {
    default void example1() {
        var content = readFile(Path.of("test.txt"));
        println("file content: " + content);
    }

    default void example2() {
        var userName = readInput();
        println("user name is: " + userName);
    }
}

The only rule is we don't invoke computation we cannot find locally! You might wondering why is interface, I will explain it in the next section. Except that, we just define our effect interfaces!

public interface Console {
    default void println(String msg) {
        System.out.println(msg);
    }
    default String readInput() {
        Scanner scanner = new Scanner(System.in);
        return scanner.nextLine();
    }
}

Above program explain how one program runs with fixed computation, but it has not say what if we want to invoke others implementation? For example, if A will invoke methods of B, what should we do?

3. Interface as computation set

It's straightforward: inheriting B!

public interface A extends B, Console {}

The program will be able to invoke methods of B, and this kind of usage is exactly why we must using interface instead class!

An important but might not clear point is, A will also inherited computation from B. For example, if B has FileSystem, then A can invoke FileSystem as well. This is a desired behavior, since if a function will involve computation somewhere, then the computation should pop up.

Another propety is, if we have two parallel inherited chain, they can have non-related effects at all. This is also important that we are freely combining these computation in the top (e.g. main function).

This eventually leads us to a very simple and helpful idea to manage IO effects.

4. Appendix

The IO monad in Java

public class IO<A> {
    State<RealWorld, A> inner;
    <T extends RealWorld> IO(State<T, A> s) {
        inner = (State<RealWorld, A>) s;
    }
    public <T extends RealWorld> Pair<A, T> run(T s) {
        return (Pair<A, T>) this.inner.run(s);
    }
    public static <T extends RealWorld> IO<T> get()
    {
        return new IO<>(State.get());
    }
    public static <T extends RealWorld> IO<One> put(T world)
    {
        return new IO<>(State.put(world));
    }
    public static <A> IO<A> pure(A a) {
        return new IO<>(State.pure(a));
    }
    public <B> IO<B> bind(Function<A, IO<B>> fact2) {
        return new IO<>(new State<>((world) -> {
            var tmp = this.run(world);
            return fact2.apply(tmp.fst).run(tmp.snd);
        }));
    }

    public static <T extends RealWorld> IO<T> mk() {
        return IO.get();
    }
    public <T extends RealWorld> IO<T> with(T world) {
        return this.bind((_w) -> IO.put(world)).bind((_one) -> IO.get());
    }
}

Footnotes:

Date: 2023-05-02 Tue 15:09
Author: Lîm Tsú-thuàn