代数的データ型のシミュレーション

タスクの完了状態として、 1. 正常に値を戻した場合 2. 例外を投げた場合 3. まだ完了していない場合 の3種類があるとします。Haskellであれば代数的データ型、OCamlであればvariant型で表すところです。Javaは、このような型を言語としては表せませんが、Visitorパターンとその変種によってシミュレーションできます。

ふつうのVisitorパターン

最初のやり方はふつうのVisitorパターンです。「完了状態」をStatus抽象クラス、個々の状態の種類を具象クラスSuccess, Failure, Unfinishedとします。

interface Visitor<T, R> {
  R visit(Success<T> success);
  R visit(Failure<T> failure);
  R visit(Unfinished<T> unfinished);
}

abstract class Status<T> {
  abstract <R> R accept(Visitor<T, R> visitor);
}

class Success<T> extends Status<T> {
  private final T result;

  Success(T result) {
    this.result = result;
  }

  T getResult() {
    return this.result;
  }

  @Override <R> R accept(Visitor<T, R> visitor) {
    return visitor.visit(this);
  }
}

class Failure<T> extends Status<T> {
  private final Exception exception;

  Failure(Exception exception) {
    this.exception = exception;
  }

  Exception getException() {
    return this.exception;
  }

  @Override <R> R accept(Visitor<T, R> visitor) {
    return visitor.visit(this);
  }
}

class Unfinished<T> extends Status<T> {
  @Override <R> R accept(Visitor<T, R> visitor) {
    return visitor.visit(this);
  }
}

class Main {
  public static void main(String[] args) {
    handle(new Success<>(42)); // => OK: 42
    handle(new Failure<>(new Exception("boo"))); // => Ouch: java.lang.Exception: boo
    handle(new Unfinished<>()); // => Unfinished
  }

  private static void handle(Status<Integer> status) {
    String msg = status.accept(new Visitor<Integer, String>() {
      @Override public String visit(Success<Integer> success) {
        return "OK: " + success.getResult();
      }
      @Override public String visit(Failure<Integer> failure) {
        return "Ouch: " + failure.getException();
      }
      @Override public String visit(Unfinished<Integer> unfinished) {
        return "Unfinished";
      }
    });
    System.out.println(msg);
  }
}

Statusのインスタンスは、3種類のうちいずれかのvisitメソッドを呼び出すので、代数的データ型をシミュレーションしているものとみなせます。

この方法の利点は、よく知られているパターンであるために、オブジェクト指向プログラミングに通じている人であれば容易に理解できることです。難点は、見ての通り冗長なことです。

Visitorパターンから具象クラスを隠蔽

前項の例でSuccess, Failure, Unfinishedの各具象クラスは、単にvisitメソッドへの振り分けを行うための、タグのような役割を果たしているに過ぎません。タグの役割をメソッド名に追い出すことで、具象クラスを表出させないようにすることもできます。

interface Visitor<T, R> {
  R visitSuccess(T result);
  R visitFailure(Exception exception);
  R visitUnfinished();
}

abstract class Status<T> {
  abstract <R> R accept(Visitor<T, R> visitor);

  static <T> Status<T> success(T result) {
    return new Status<T>() {
      @Override <R> R accept(Visitor<T, R> visitor) {
        return visitor.visitSuccess(result);
      }
    };
  }

  static <T> Status<T> failure(Exception exception) {
    return new Status<T>() {
      @Override <R> R accept(Visitor<T, R> visitor) {
        return visitor.visitFailure(exception);
      }
    };
  }

  static <T> Status<T> unfinished() {
    return new Status<T>() {
      @Override <R> R accept(Visitor<T, R> visitor) {
        return visitor.visitUnfinished();
      }
    };
  }
}

class Main {
  public static void main(String[] args) {
    handle(Status.success(42)); // => OK: 42
    handle(Status.failure(new Exception("boo"))); // => Ouch: java.lang.Exception: boo
    handle(Status.unfinished()); // => Unfinished
  }

  private static void handle(Status<Integer> status) {
    String msg = status.accept(new Visitor<Integer, String>() {
      @Override public String visitSuccess(Integer result) {
        return "OK: " + result;
      }
      @Override public String visitFailure(Exception exception) {
        return "Ouch: " + exception;
      }
      @Override public String visitUnfinished() {
        return "Unfinished";
      }
    });
    System.out.println(msg);
  }
}

よく見る変種のわりには、名前がついていないのが気になります。「Visitor-AnonymousElementパターン」とか?

Visitorインタフェースを関数に分解

Visitorインタフェースは、3つの関数の三つ組として分解することも可能です。

import java.util.function.Function;
import java.util.function.Supplier;

abstract class Status<T> {
  abstract <R> R accept(
      Function<T, R> visitSuccess,
      Function<Exception, R> visitFailure,
      Supplier<R> visitUnfinished);

  static <T> Status<T> success(T result) {
    return new Status<T>() {
      @Override <R> R accept(
          Function<T, R> visitSuccess,
          Function<Exception, R> visitFailure,
          Supplier<R> visitUnfinished) {
        return visitSuccess.apply(result);
      }
    };
  }

  static <T> Status<T> failure(Exception exception) {
    return new Status<T>() {
      @Override <R> R accept(
          Function<T, R> visitSuccess,
          Function<Exception, R> visitFailure,
          Supplier<R> visitUnfinished) {
        return visitFailure.apply(exception);
      }
    };
  }

  static <T> Status<T> unfinished() {
    return new Status<T>() {
      @Override <R> R accept(
          Function<T, R> visitSuccess,
          Function<Exception, R> visitFailure,
          Supplier<R> visitUnfinished) {
        return visitUnfinished.get();
      }
    };
  }
}

class Main {
  public static void main(String[] args) {
    handle(Status.success(42)); // => OK: 42
    handle(Status.failure(new Exception("boo"))); // => Ouch: java.lang.Exception: boo
    handle(Status.unfinished()); // => Unfinished
  }

  private static void handle(Status<Integer> status) {
    String msg = status.accept(
        result -> "OK: " + result,
        exception -> "Ouch: " + exception,
        () -> "Unfinished");
    System.out.println(msg);
  }
}

acceptの呼び出しが簡潔になる反面、状態の名前が明示されないことが難点です。とはいえ、状態の種類が少数にとどまるのであれば、これで十分かもしれません。

ここまでくると、Visitorパターンの変種というよりも、代数的データ型をdesugarしたもの、という感じです。