Javaの例外で戻り値を表現してみたらどうなりそうか?

2024/03/03に公開

以前から「何を例外であらわすべきか、何をそれ以外であらわすべきか」があまりよく分かっておらずモヤモヤしていました。私も例外が好きなのですが、「それなら普通は戻り値で返すものも例外で表現したらどうなるか」と思うにいたり、どうなりそうか考えてみました。「何を例外であらわすべきか、何をそれ以外であらわすべきか」の参考になりましたら幸いです。

最終的には、「逆ポーランド記法の式(1 2 + => 3 みたいな)を計算する電卓」を作ってみようと思います。

(本文書は別のところに上げた記事のブラッシュアップ版です)

戻り値の型の定義

今回は「例外で関数の戻り値を表現する」という話なので、まず下記のような定義をしておきます。この型は Exception を拡張して定義しておきます。

// 数値型をあらわす型
class IntType extends Exception {
    int v;
}

メソッド定義

メソッド定義は、下記のように void 関数名(引数リスト) throws 戻り値の型 { 処理 } と定義できそうです。今回定義した例外はチェック例外で throws のあとに型を記載します。これで戻り値の型を明示できました。

void eval(String s) throws IntType {
  /* 処理 */
}

図らずも、メソッド定義の語順が最近の他の言語(RustやKotlin, Scala, Go)に近くなった感じがします。例えばScalaでは、下記のように def 関数名 ( 引数リスト ): 戻り値の型 = 処理 という語順ですし、Rustも関数を下記のように fn 関数名 (引数リスト) -> 戻り値の型 という語順です。
つまり今回のJavaのスタイルでは、関数であることをあらわすキーワードとして deffn ではなく void と書いて定義を始めるといった調子になります。

Scala:

// Scala
def eval(x: String): Int = {
  /* 処理 */
}

Rust:

// Rust
fn five() -> i32 {
  // 処理
}

戻り値の型のユニオンタイプ化

例外を戻り値として使うと、ユニオンタイプの戻り値を表現できます。
例えば「数値型」と「演算子型」とのユニオンタイプがあれば、電卓アプリの開発で便利そうです。

void eval(String s) throws IntType, PlusOperation{
  /* 処理 */
}

上記のようにthrows のあとに複数の型を列挙することで、戻り値の型がユニオンタイプになったとみなせるのではないでしょうか。

値を返す

値を返すために、多くのプログラミング言語で return としているところを throw に置換します。

  throw new IntType(1);

このようにして数値型を返します。
演算子型を返す場合は、throw new PlusOperation(); のようにします。

型に応じた分岐

例えばScala だと match/case というものがあります:

// Scala
something match {
  case a: IntType => "Int"
  case b: PlusOperation => "+"
}

戻り値を例外で表すようにしたことでJavaでも同様のことができるようになります。
try/catch により戻り値の型に応じた条件分岐ができます:

try {
    eval(s);
} catch (IntType n) {
    // で数値型に応じた処理
} catch (PlusOperation op) {
    // でプラス演算子の型に応じた処理
}

今まではJavaではビジターパターンを使ったり、instanceof(..) で型を調べて分岐させたりしていました。例外を戻り値として使うことで、最近の言語のスタイルに近い形で型に応じた処理を書けるようになりました。

ちなみにこのスタイルのメリットとして、一時変数を使うことが減って throwtry/catch のブロックの組み合わせで処理を組み立てていくことになります。このため関数型プログラミングのスタイルに近づけられます。

エラーチェック

エラーチェックは下記のように catch を追加することでハンドリングします。

try {
  parseInt(s);// →IntType,NumberFormatException
} catch (NumberFormatException e) {
  // 戻り値がエラーのときのハンドリング処理
} catch (IntType n) {
  throw n  // エラーがない場合はそのまま数値を返す
}

サンプルコード

こうして「逆ポーランド記法の式 (1 2 + => 3 みたいな) を計算する電卓」が作れました:

package neta;

import java.util.Stack;

public class Main {
    // 数値型をあらわす型
    static class IntType extends Exception {
        int v;
        IntType(int v) {
            this.v = v;
        }

        @Override
        public String toString() {
            return Integer.toString(v);
        }
    }

    // プラス演算をあらわす型
    static class PlusOperation extends Exception {
        @Override
        public String toString() {
            return "+";
        }
    }

    private void eval(String statement) throws IntType, PlusOperation {
        try {
            parseInt(statement);
        } catch (NumberFormatException e) {
            if (statement.equals("+")) {
                throw new PlusOperation();
            } else {
                // Stackを使って逆ポーランド記法の計算をします
                Stack<Integer> stack = new Stack<>();
                // トークンは空白区切りであたえます
                for (String s : statement.split(" ")) {
                    try {
                        eval(s);
                    } catch (IntType iv) {
                        stack.push(iv.v);
                    } catch (PlusOperation op) {
                        stack.push(stack.pop() + stack.pop());
                    }
                }
                throw new IntType(stack.pop());
            }
        } catch (IntType n) {
            throw n;
        }
    }

    private void parseInt(String s) throws IntType, NumberFormatException {
        throw new IntType(Integer.parseInt(s));
    }

    private static void test(String statement) {
        try {
            new Main().eval(statement);
        } catch (IntType | PlusOperation e) {
            System.out.println(e);
        }
    }

    public static void main(String[] args) {
        test("1");
        test("1 2 +");
        test("3 4 + 5 +");
        test("6 7 8 + +");
        test("+");
    }
}

このコードからは次のように出力されます:

1
3
12
21
+

ありがとうございました。

その他

  • サンプルコードはJava 8以上でコンパイル、実行できるはずです。
  • 本文書は別のところに上げた記事のブラッシュアップ版です。

Discussion