著作一覧 |
仕事環境でJDK1.8を解禁させたので、いろいろ試しているうちにとんでもないことに気付いた。簡単にインターフェイスが爆発してしまうのだ(あまりうまい表現ではないなぁ。ビッグバンが起きるという雰囲気を出したいのだが)。
ようするに、これまで糞面倒だったコールバックが異様に簡単に書けるようになったので、呼び出し側が簡単に利用できる以上は呼ばれる側も情け容赦なくコールバック関数を取るAPIを作るわけだが、異様に簡単なのはラムダ式を記述できるからだ。ということは、関数型インターフェイスをばかすか定義することになる。
そんなものは1メソッドあたりで定義すれば良いし、呼び出し側はどうせ型宣言とかしないのだから、内部インターフェイスで良いだろうと考える。すると1メソッド平均1.5インターフェイス、1クラスあたり5publicメソッドとすると、1クラスあたり8インターフェイスくらいが定義されてしまう。それが累積するのですごいことになる(クラスファイルは)。
たとえばHttpUrlConnectionをラップした便利クラスを作るとする。ここでの便利というのはドメイン特化と言う意味だ(でも下のサンプルは汎用だけど)。
public class EasyHttp implements Closeable { HttpURLConnection connection; public EasyHttp(String uri) throws Exception { this(uri, null, null); } public EasyHttp(String uri, String user, String pwd) throws Exception { URL url = new URL(uri); connection = (HttpURLConnection)url.openConnection(); if (user != null) { connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString((user.trim() + ":" + pwd.trim()).getBytes())); } connection.setRequestProperty("Accept-Encoding", "gzip"); } public interface SetupCallback { void setup(HttpURLConnection c) throws Exception; } public void setup(SetupCallback cb) throws Exception { cb.setup(connection); } public interface WriteOperation { void write(OutputStream os) throws IOException; } public interface ReadOperation { void read(InputStream is) throws IOException; } public interface ReadStringOperation { void read(String response) throws Exception; } public interface ErrorOperation { void error(int code, InputStream is) throws Exception; } public int get(ReadOperation ro, ErrorOperation eo) throws Exception { return start(null, ro, null, eo); } public int getString(ReadStringOperation ro, ErrorOperation eo) throws Exception { return start(null, null, ro, eo); } public int post(WriteOperation wo, ReadOperation ro, ErrorOperation eo) throws Exception { connection.setDoOutput(true); return start(wo, ro, null, eo); } // この名前は悪い。StringをPOSTするみたいだ。getStringに合わせてレスポンスをStringで取るという意味だがそうは読めない public int postString(WriteOperation wo, ReadStringOperation ro, ErrorOperation eo) throws Exception { connection.setDoOutput(true); return start(wo, null, ro, eo); } int start(WriteOperation wo, ReadOperation ro, ReadStringOperation rso, ErrorOperation eo) throws Exception { connection.connect(); if (wo != null && connection.getDoOutput()) { try (OutputStream os = connection.getOutputStream()) { wo.write(os); os.flush(); } } String encoding = connection.getHeaderField("Content-Encoding"); boolean gzipped = encoding != null && encoding.toUpperCase().equals("GZIP"); int status = connection.getResponseCode(); if (status == HttpURLConnection.HTTP_OK) { try (InputStream is = (gzipped) ? new GZIPInputStream(connection.getInputStream()) : connection.getInputStream()) { if (ro != null) { ro.read(is); } else { ByteArrayOutputStream bao = new ByteArrayOutputStream(); byte[] buff = new byte[8000]; for (;;) { int len = is.read(buff); if (len < 0) { break; } else if (len == 0) { continue; } bao.write(buff, 0, len); } if (rso != null) { rso.read(bao.toString("UTF-8")); } bao.close(); } } } else if (eo != null) { try (InputStream is = (gzipped) ? new GZIPInputStream(connection.getErrorStream()) : connection.getErrorStream()) { eo.error(status, is); } } return status; } @Override public void close() throws IOException { if (connection != null) { connection.disconnect(); } } }
JDK1.8の型推論がいまいちなのは、上の例だとget, getStringなどとユーザーAPIのメソッド名をオーバーロードではなく別物にせざるを得ない点だ。つまりもし同じ名前にすると、仮にラムダ式の内側で型が明らかに異なっても「参照はあいまいです」というエラーになる点だ。で、しょうがないのでメソッドはオーバーロードせずに名前を変えざるを得ない。
で、とにかくまともになったのは、上の細かなインターフェイス名などをいちいち呼び出し側は書く必要が無い点だ。
// なんとあの見苦しいimport java.io.*をほとんど書く必要がない。 try (EasyHttp eh = new EasyHttp("http://www.yahoo.co.jp")) { eh.getString(s -> System.out.println(s), (code, es) -> System.out.println("error:" + code)); } catch (Exception e) { e.printStackTrace(); }
あるいは
try (EasyHttp eh = new EasyHttp("http://example.com/postdata.aspx")) { eh.setup(c -> c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")); eh.postString(os -> os.write("a=b&c=d".getBytes()), s -> System.out.println(s), (code, e) -> System.out.println("error:" + code)); } catch (Exception e) { e.printStackTrace(); }
呼び出し側は、これがJavaかと驚くほどシンプルに書けるようになって、実に良い。しかしclassesディレクトリを見るとぞっとするほどclassファイルができている。
でちょっと思ったのは、少なくとも楽に関数を引数にできると、テンプレートメソッドパターンはほとんど必要なくなるので(インスタンス変数を持つ側の問題があるからすべてではない)、それほど継承(一番のモチベーションはテンプレートメソッドパターンの適用なので)を使う必要がなくなる。それでJavaScriptはクラスベースではないのに、元々のJavaよりも使いやすいのかな? ということだったりする(今となってはJavaのほうが関数引数は書きやすいわけだが)。
ジェズイットを見習え |
java.util.functionパッケージで定義されている汎用のConsumerインタフェースやFunctionインタフェースなどを使えば、ほとんどの関数型インタフェースは定義する必要はないと思うのですが....<br>ラムダ式にしてしまった時点でインタフェース名を記述する必要がなくなるので、固有の関数型インタフェースを定義する必要性は感じないです。
Javadocはどうするの? というかドキュメントがないと使えないと思うんだけど(もちろん、別紙で提供という方法はあるとは思うが)。
(これってActionやProcでも感じた(なんか気分的にC#だとこっちのルーズなやつでOKと感じる)疑問なんだが、関数パラメータの意味をどこに持たせるかってことだな)
関数型インタフェースを引数にとるメソッドのJavadocをちゃんと書けばいいのではないでしょうか。
そうは思わないんだけど。というのは、引数に取る側のメソッドと、引数に取られる関数は異なるものですよね? (そう思わないならそこまでだけど)<br>で、その引数は型によって意味づけられている。であれば、その型のドキュメントに説明されるべきです。そうでなければ型付けの意味がない。意味は無視して便宜優先というのであれば、こでもそこまでですね。
結局のところ関数型インタフェースを引数にとるメソッドで、その関数型インタフェースの用途やパラメータの意味は変わってしまって、意味は利用側のメソッドのほうで持つので、結局メソッド側で記述しないといけない気がします。<br>むしろ、個別にインタフェースを定義するよりも、共通のFunctionやConsumerなどのインタフェースを使ったほうが、意味がわかりやすくなるように思います。<br>もちろん、名前をつけたほうがわかりやすくなるケースもありうるとは思います。
オーバーロードの問題は、和型(String + IntStreamのような)とパターンマッチの欠如から来てるかなーという気が。
オーバーロードの問題は当然だと思います。この例では、System.out.printlnの引数にしか利用していないのだから、StringかInputStreamか、コンパイラに区別がつくはずがあり得ない。<br>それを型推論みたいな用語で説明しているの見かけてなんだかなぁと気になってたんだよね(で試したら案の定推論できずにエラーになって予想通りではあったけど)。<br>引数の型についてはやっぱり違うんだなぁ。ドキュメントがメソッド側にあったら(ドキュメントを含む)モジュール化できないじゃん。<br>(はっ、そのためにオーバーロードできない仕掛けになっているのか。じゃあ整合性が取れているからしょうがないのだな)
実際的な話でいえば、この1年半くらい実務でいろいろJava8でコードを書いて、FunctionalInterface側のドキュメントを読もうと思ったことがない、そこを見てなにか解決する可能性があると思ったことがない、というのはあります。
同一ソース内の型はJavadocにリンクが入るから簡単に飛べるよ。あとろくにドキュメントされていないことが多かったから読もうと思ったことが無いんじゃないかな。書くべき場所を間違えた例ばかり見ていれば、それは当然そうなると思います。
いや、特にその情報が欲しいと思ったことがないのです。引数・戻り値の組と、そのメソッド自体の説明があれば、引数・戻り値の組に対する説明は欲しいと思わないしあってもそれで解決するように思えないのです。
なるほど。それならいいですね。