Reader Monad in Java

Reader

import java.io.*;
import java.util.*;
import java.util.function.*;

class Reader<R, A> {

  private final Function<R, A> k;

  A run(final R r) {
    return this.k.apply(r);
  }

  Reader(final Function<R, A> k) {
    this.k = k;
  }

  static <A, B> Reader<A, B> fromFunction(final Function<A, B> k) {
    return new Reader<>((a) -> k.apply(a));
  }

  static <A> Reader<A, Void> fromConsumer(final Consumer<A> k) {
    return new Reader<>((a) -> {
      k.accept(a);
      return null;
    });
  }

  <R2 extends R, B> Reader<R2, B> map(final Function<A, B> f) {
    return new Reader<>((r) -> f.apply(run(r)));
  }

  <R2 extends R, B> Reader<R2, B> flatMap(final Function<A, Reader<R2, B>> f) {
    return new Reader<>((r) -> f.apply(run(r)).run(r));
  }

  <R2 extends R, B> Reader<R2, B> andThen(final Reader<R2, B> next) {
    return new Reader<>((r) -> {
      run(r);
      return next.run(r);
    });
  }
}

Usage

interface GetEnv {

  Map<String, String> getEnv();

  static <R extends GetEnv> Reader<R, Map<String, String>> of() {
    return Reader.fromFunction((r) -> r.getEnv());
  }
}
interface ReadLine {

  String readLine();

  static <R extends ReadLine> Reader<R, String> of() {
    return Reader.fromFunction((r) -> r.readLine());
  }
}
interface WriteLine {

  void writeLine(final String line);

  static <R extends WriteLine> Reader<R, Void> of(final String line) {
    return Reader.fromConsumer((r) -> r.writeLine(line));
  }
}
class Greeter {

  static <R extends ReadLine & WriteLine> Reader<R, Void> esProgram() {
    return WriteLine.of("¿Cómo te llamas?")
      .andThen(ReadLine.of())
      .flatMap((name) -> WriteLine.of(String.format("¡Hola, %s!", name)));
  }

  static <R extends GetEnv & ReadLine & WriteLine> Reader<R, Void> enProgram() {
    return WriteLine.of("What is your name?")
      .andThen(ReadLine.of())
      .flatMap((name) -> WriteLine.of(String.format("Hello, %s!", name)));
  }

  static <R extends GetEnv & ReadLine & WriteLine> Reader<R, Void> program() {
    return GetEnv.of()
      .flatMap((env) -> {
          final String lang =
            Optional.ofNullable(env.get("LANG"))
              .map((x) -> x.substring(0, 2))
              .orElse("en");
          switch (lang) {
            case "es": return esProgram();
            default: return enProgram();
          }
      });
  }
}

Live

class Live {

  interface Env extends GetEnv, ReadLine, WriteLine { }

  static Env env =
    new Env() {
      @Override
      public Map<String, String> getEnv() {
        return System.getenv();
      }
      @Override
      public String readLine() {
        final java.io.BufferedReader r =
          new BufferedReader(
            new InputStreamReader(System.in));
        try {
          return r.readLine();
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      }
      @Override
      public void writeLine(final String line) {
        System.out.println(line);
      }
    };
}

Test

class Test {

  static final LinkedList<String> output =
    new LinkedList<>();

  static final LinkedList<String> input =
    new LinkedList<>(Arrays.asList("James"));

  interface Env extends GetEnv, ReadLine, WriteLine { }

  static Env env =
    new Env() {
      @Override
      public Map<String, String> getEnv() {
        return Map.ofEntries(Map.entry("LANG", "en_US.UTF-8"));
      }
      @Override
      public String readLine() {
        return input.pop();
      }
      @Override
      public void writeLine(final String line) {
        output.push(line);
      }
    };
}

Demo

public class Main {
  public static void main(String[] args) {
    if ("live".equalsIgnoreCase(System.getenv("ENV"))) {
      Greeter.program().run(Live.env);
    } else {
      Greeter.program().run(Test.env);
      System.out.println(String.format("produced: %s", Test.output));
      System.out.println(String.format("unconsumed input: %s", Test.input));
    }
  }
}

This file is literate Java, and can be run using Codedown:

$ curl https://earldouglas.com/posts/effect-systems/reader-java.md |
  codedown java > Main.java
$ javac Main.java
$ ENV=live LANG=es java Main
¿Cómo te llamas?
James
¡Hola, James!