はじめに
Java さわってますか?私は10年近くさわってません。JDK 8 Release Notesを見ると2014年5月15日にリリースされているので大体10年です。
この10年位は golang をさわってます。
Javaの勉強もしなければなと思いつつも、作りたいアプリケーションも特にありません。ドキュメントを読んでるだけというのも長続きしないかなと思ってます。そんな時に以下の2つのブログを読みました。
これらを読んで「 Java の Optional を golang に移植したら、 Java のコードも読むし勉強になるかな?」と思いました。そんなわけでゆるゆると Java の Optional を golang に移植していこうと思います。その際に得た知見をゆるゆるとブログに書いていきます。
Java の Optional の書き方はあまりgolang的なものではないなと思ってます。なので、「これがあれば幸せになれるよ」的なものを作ろうとしている訳ではありません。あくまでネタとして、ゆるゆる見て頂ければと思います。
移植対象
移植対象の Optional はJDK 21 にしました。具体的には以下のソースコードです。
また JavaDoc は以下のものになります。
また Optional ではいかの interface が利用されています。
これらも適宜実装していきます。前振りが長くなりましたが、ここから本編です。
Functionの実装
Optional の移植といいつつ、まずは Function を実装します。 Optional のメソッドを実装するために必要なため、 Function から始めます。Javaのソースコードはこちらです。 以下にコメントを削除したコードを記載します。
package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
このコードでgolangでは直接対応出来無い部分は以下の3つになります。
- メソッドに対するgenericの適用
- default method の実装
- static method の実装
この中で特に問題になるのが 1 です。例えば以下の interface を golang で定義したとします。
type Function[T any, R any, V any] interface {
Apply(T) R
AddThen(Function[R, V, T]) Function[T, V, R]
Compose(Function[V, T, R]) Function[V, R, T]
}
形は Java の Function
と似ていますが、実際は全く異るものです。
Java の場合は compose
と addThen
というメソッドに対して V
という型パラメータが指定されています。このため、この V
は compose
や addThen
が利用される時に決定されます。複数の個所で異る V
と使っても問題有ません。ある場所では Integer
でも、別の場所では String
を利用できます。勿論、 compose
と addThen
では別の V
を利用できます。
これに対して golang では V
は Function
interface に対して指定されています。このため、 Function
interface を実装する構造体を実装する際に決定されなければなりません。 Java とは異り、 Compose
と AddThen
は同じ V
を使わなければなりません。また Compose
と AddThen
は使われる全ての場所で同じ V
を使わなければなりません。
これは、 golang がコンパイル時に型パラメータと対応した構造体やinterfaceを生成しているのに対し、 Java はコンパイル時に型のチェックを行っているだけという違いから生れるものだと思います。 golang では設定されたそれぞれの型パラメータに対して対応する実体が生成されます。例えば slices.Concat に対して
slices.Concat([]int{1, 2}, []int{3})
slices.Concat([]string{"foo", "bar"}, []string{"buz"})
は別の関数が生成されます。それにたいして Java の ArrayListでは、
new ArrayList<Integer>()
new ArrayList<String>()
をした場合、別のクラスは生成されません。あくまで ArrayList<Object>
が存在するだけです。それぞれは利用されている場所で Integer
や String
のみを受け付けるようコンパイル時にチェックされるだけです。
実際にバイトコードではこの情報は削除されます。この削除をType erasureと呼びます。この結果、 Java ではメソッドに型パラメータが指定されていたとしても、利用されないならば、無視してコンパイルできることになります。 andThen
が使われない場合は、andThen
の戻値の Function<T, R>
の R
部分は存在しなくてもいいわけです。しかし golang の場合は、利用しないとしてもその型パラメータの指定は必要になります。それがなければ interface を生成できないからです。
長々書きましたが、結論としては golangだと andThen や compose 相当のメソッドは定義できないので、関数にする
という結論になりました。
1.の メソッドに対するgenericの適用
だけでかなりながくなりまし。 2. default method の実装
については今回はdefault methodを使わなくて済んだので、今後必要となった時に考えます。3. static method の実装
については関数で対応します。
上記をふまえて実装した Java
の Function
に対応する機能が以下のものになります。
Functionとの相違点をまとめると
- interface名は
Applier
にした- 残ったメソッドが
apply
のみだったため、golangの慣習に従った
- 残ったメソッドが
- Java のメソッド
andThen
とcompose
はCompose
関数に纏めた- メソッドだから前に付けるか後ろに付けるかが問題になる。関数ならば問題にならない
- satic method の
identity
も関数のIdentity
にした -
NewApplierFunc
とNewApplierFuncWithNoError
を導入し、関数を簡単にApplier
interface へ適合できるようにした- Javaの場合は FunctionalInterface を満していればメソッドを簡単に
Function
interfaceに代入できるが、golang
ではそれができないため
- Javaの場合は FunctionalInterface を満していればメソッドを簡単に
まとめ
Function
の移植だけでもかなりの調べることがありました。色々調べているつもりでしたが、全然足りていないと実感しました。公式ドキュメントをちゃんと調べるいい機会でした。
今後は Predicate
などを移植していきます。Javaとgolangの仕様を調べないといけないので、楽しみです。
参考文献
つづき
JavaのOptionalをgolangにportする - Method Reference と Method values