JavaでTupleってみる

可変長な型変数の表現より、タプルの話です。

.NETのタプル

.NETでは、型引数の数が違うクラスを定義できるので、Tupleという名前で7要素まで対応しています。

public class Tuple<T1> { ... }
public class Tuple<T1, T2> { ... }
public class Tuple<T1, T2, T3> { ... }
public class Tuple<T1, T2, T3, T4> { ... }
public class Tuple<T1, T2, T3, T4, T5> { ... }
public class Tuple<T1, T2, T3, T4, T5, T6> { ... }
public class Tuple<T1, T2, T3, T4, T5, T6, T7> { ... }

そして、各要素にアクセスするために、Item1, Item2のようなプロパティを使います。

さて、では.NETのタプルは8要素以上は対応していないかと言うと、そうではありません。 タプルのインスタンスを作るためには、Tuple.Createというメソッドを使って作るのが楽です。 そしてこのTuple.Createは、なんと8引数版まで用意されているのです。

var t = Tuple.Create(true, 2, 3.0, "4", new[] { 5 }, 6M, 7L, 8);

このtの型は、こうなります。

Tuple<bool, int, double, string, int[], decimal, long, Tuple<int>>

実は、Tupleは8番目の型引数として、「残りの要素を保持するタプルの型」が受け取れるようになっています。 ちなみにTuple<T1>という謎のクラスはこの時のみ使われるクラスです。

public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> { ... }

「残りの要素を保持するタプル」にアクセスするためには、Item8ではなく、Restという名前のプロパティを使います。 そのため、8要素タプルの8番目の要素にアクセスしたい場合は、

var value = t.Rest.Item1;

となります。 さてさて、ここからが.NETの残念なところなのですが、実は9要素以上のタプルを作る簡単な方法は用意されていません*1。 ので、コンストラクタを使うことになります。

var t = new Tuple<int, int, int, int, int, int, int, Tuple<int, int>>(
    1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9));

コンストラクタの型パラメータは省略できないので、悲惨なことになっていますね・・・ これは、Tuple.Createの8要素版が8要素タプルを作るためではなく、ネストしたタプルを作るためのヘルパーになっていればもっと楽が出来たのです。

var t = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9)); // こうはなっていない

残念すぎる・・・

Restという考え方

さて、.NETのタプルは残念ですが、「足りない部分はRestで」というのはどこかで聞いたことのあるような話です。 そう、コンスセルですね!

ということで、コンスセルの考え方を使ってタプルのないJavaにタプルを作ってみましょう。 まずは、コンスセルの終端を表すためのクラスを導入します。 以降では、特にパッケージとか書きませんけど、全部同一パッケージに入っていると思ってください。

public final class TupleNil {
    static final TupleNil nil = new TupleNil();
    private TupleNil() {}
}

簡単ですね。 こいつは状態を持っておらず、インスタンスも外部からは生成できず、 さらには唯一の静的フィールドであるnilもパッケージプライベートなので、 パッケージ外からはこのフィールドにすらアクセスできません。 これだけでは本当に全く何の役にも立たないクラスです。

次に、2つの型パラメータを取って実際に要素を保持するクラスを作ります。

public final class TupleCons<T, Rest> {
    public final T value;
    public final Rest rest;

    TupleCons(T value, Rest rest) {
        this.value = value;
        this.rest = rest;
    }
}

これも簡単ですね。 単に、値を2つ持っているだけのクラスです。 このクラスのvalueに値が、restに残りの要素を表すものが格納されます。

コンストラクタがパッケージプライベートなので、パッケージ外からこのクラスのインスタンスを生成することはできません。

最後にタプルを作るためのメソッドを持ったクラスです。

public final class Tuple {
    private Tuple() {}

    public static <T> TupleCons<T, TupleNil> singleton(T value) {
        // こことか
        return new TupleCons<T, TupleNil>(value, TupleNil.nil);
    }

    public static <T1, T2, Rest> TupleCons<T1, TupleCons<T2, Rest>> cons(T1 value, TupleCons<T2, TupleNil> rest) {
        // ここってダイアモンド演算子使えるんですか?使えそうだけどJavaとかわかりません><
        return new TupleCons<T1, TupleCons<T2, Rest>>(value, rest);
    }
}

あとは、これを使ったコードです。

TupleCons<Integer, TupleCons<String, TupleNil>> t2 = Tuple.cons(42, Tuple.singleton("hoge"));
System.out.println(t2.value); // 42
System.out.println(t2.rest.value); // hoge

やったー!(何が

まとめ(ない)

  • .NETのタプルは割と現実見て、7要素までは自然に扱える
    • 大量要素のタプル使うな
    • でももし扱う場合があるといけないから、一応対応しとくぜ!
  • .NETのTuple.Createはクソ
    • 8要素版をなぜそういう形で用意したし
    • 9要素以上のタプルを作りたい場合はコンストラクタで頑張るしかない
    • それLangExtならCreate.Tuple(1, 2, 3, 4, 5, 6, 7, 8, 9)でできるよ!
    • それF#なら(1, 2, 3, 4, 5, 6, 7, 8, 9)でできるよ!
  • タプルってコンパイル時のリスト(コンスセル)だよね!
    • Javaでそういう実装してみた
    • 元記事が後ろに後ろに拡張していくのに対して、こっちは(consで)前に前に拡張していくという違いがちょっと面白い
    • 元記事がインスタンスメソッドで拡張していくのに対して、こっちはクラスメソッドで拡張していくのもちょっと面白い
      • インスタンスメソッドにもできるけど、キモイことになる(字面上は後ろに書いたものが前に追加される)のでやめた
    • 実用性?しりませんなぁ

*1:まぁ、そんなタプル作るなと言われればその通り、としか言えないですが