ProjectLambda・5部(Tips)
ここで使用されているサンプルは、lambda-8-b25-05_feb_2012に基づいて作成されています。現在の仕様とは違っている点があるかもしれませんので注意してください。
ここまでラムダ式、メソッド・コンストラクタ参照、デフォルト実装と見てきて一通りProject Lambdaの仕様を確認しましたので、ここからはこれらのTipsを紹介していきましょう。
再帰
public void method() { method(); }
普通にJavaで書いていれば、このように再帰元のメソッドと呼び出すメソッドが等しければ再帰呼び出しをすることができましたが、実はラムダ式を用いるとちょっと困ったことが起きます。
上の例と同じ感じで
Functional0<Void> f = n -> invoke();
と書くと、コンパイルが通らないか、意図した結果にならないでしょう。
というのも、ラムダ式内でのスコープはラムダ式外のスコープと同じスコープが適応されるのでした。
なので、invoke()の部分は、ラムダ式外でinvokeを呼び出したのと同じことになり、再帰呼び出しにはならないのです。
public class Main { public static void main(String[] args) { Functional0<Void> f = () -> invoke(); // invoke()で呼び出してるのはMain#invoke } public static void invoke() { System.out.println("Main#invoke"); } }
ラムダ式で再帰呼び出しを行う場合は次のように変数を介して呼び出す必要があります。
Fucntional0<Void> f = n -> f.invoke();
ただ、これでも困ったことがまだあります。
メソッドの引数にラムダ式を用いる場合です。
public void method(Functional0<Void> f) { f.invoke(); } public void call() { this.method(() -> invoke()); // NG }
この場合でも同様に一回変数に代入してから呼び出します。
public void call() { Functional0<Void> f = () -> f.invoke(); this.method(f); }
せっかくの型推論が台無しですね。
もうちょっと別の仕組みがあってもいいのになぁ
デフォルト実装された他のメソッドをラムダ式内から呼ぶ場合も同じです。
interface I { void invoke(); void m() default {} } I i = () -> i.m();
関数の部分適用
関数型言語ではなじみ深い部分適応をやってみましょう。
まず初めに関数の部分適応を説明しておきましょう。
部分適応というのは簡潔に言うとある関数の引数の一部を定めてそれ以外の引数を受け取る関数オブジェクトを作ることです。
コードで言うと
def pow(x : Int, n : Int) : Int = if (n == 0) 1 else x * pow(x, n - 1) // xのn乗を計算するpowメソッドを定義 val pow2 : (Int) => Int = pow(_, 2) // powメソッドを部分適応して、第一引数の2乗を計算するpow2を作成 var n = pow2(2) // 4 n = pow2(3) // 9
と、まぁこんな具合です。
これに近いことをJavaでするには関数型インターフェースにデフォルト実装を使って部分適応するメソッドを追加するのが美しいと思います。
interface Functional0<R> { R invoke(); } interface Functional1<R, A1> { R invoke(A1 a1); Functional0<R> partial1(A1 a1) default { return () -> this.invoke(a1); } } interface Functional2<R, A1, A2> { R invoke(A1 a1, A2 a2); Functional1<R, A2> partial1(A1 a1) default { return (a2) -> this.invoke(a1, a2); } Functional1<R, A1> partial2(A2 a2) default { return (a1) -> this.invoke(a1, a2); } Functional0<R> partial1_2(A1 a1, A2 a2) default { return () -> this.invoke(a1, a2); } } // 以下略
こんな感じで作っておけば、Scalaの例と同じようにpowに関して、
Functional2<Double, Double, Double> pow = Math#pow; Functional1<Double, Double> pow2 = pow.partial2(2); int n = (int)pow2.invoke(2); // 4 n = (int)pow2.invoke(3); // 9
と似たようなことが出来ます。
ちなみに引数の全ての組み合わせを網羅するようなpartialはFunctional10(10個引数を取る)だと1023個あります。
20個の引数を取るようなFunctional20だと2^20 - 1個なので、ここまで来るとかなり非現実的ですwww
ターゲット型付け、ターゲット型
注:ここに書いてあることの多くが2012/5現在まだ実装されていませんので注意してください。ここでは仕様に基づいて話を進めます。
ラムダ式の説明などでサラッと触れたターゲット型付け[TargetTyping]についてもう少し細かい話をしましょう。
まず、ターゲット型付けというのはターゲットとなる型(ターゲット型)に基づいて型推論するシステムの事です。
ターゲット型があるコンテキストは
・変数代入
・キャスト
・実引数
などです。
ダイアモンドオペレータやメソッドジェネリクスの戻り値による型推論などもこのターゲット型付けに位置します。
ターゲット型でラムダ式やメソッド・コンストラクタ参照がインスタンス化されるのは先に説明したとおりですが、ターゲット型付けを強化することによって今まではコンパイルエラーになったことがJava8では出来るようになります。
例えば次のようなものです。
{ public <T> T createNull() { return null; } public String createEmptyString() { return ""; } String s = bool ? createNull() : createEmptyString(); // Java7以前ではエラー Functional0<Void> f = bool ? () -> {} : () ->{}; }
条件式では、従来はターゲット型による型推論が働きませんでしたが、Java8からは働いてくれます。
さらに、メソッド等の引数をターゲット型とする型推論も強化されます。
Java7で導入されたダイアモンドオペレータをメソッド等の引数で使用することはできませんでしたがJava8からは使用することが出来ます。
{ public <T> List<T> addToList(T element, List<T> list) { list.add(element); return list; } List<String> list = addToList("firstElement", new ArrayList<>()); // Java7ではダイアモンドオペレータでエラーになった }
あるいは、複雑に絡みあったジェネリックなメソッドの呼び出しでも型推論してくれるようになります。(当然ターゲット型によって解決できる場合のみですが・・・)
{ public static <T> List<T> addToList(List<T> list, T t) { list.add(t); return list; } static String str = addToList(new ArrayList<>(), null).get(0); // Java7以前ではaddToListのTがObjectと推論されエラー // この場合getの方から推論できるのでJava8ではOK public static <T> List<T> createEmptyList() { return new ArrayList<>(); } static List<String> list = addToList(createEmptyList(), "firstElement"); // "firstElement"や戻り値からaddToListのTが定まり、createEmptyListのターゲット型(T)からcreateEmptyListのTが定まる。 }
実型引数に合成された型
合成された型というのは、仮型引数の型境界で言うところの&で繋がれた奴。
これを直接的に実型引数に指定することは出来ません。*1
なので次のようなプログラムだとちょっと困ったことが起きます。
interface I1 { void m1(); } interface I2 { void m2(); } class A implements I1, I2 { public void m1() { System.out.println("A#m1"); } public void m2() { System.out.println("A#m2"); } } class B implements I1, I2 { public void m1() { System.out.println("B#m1"); } public void m2() { System.out.println("B#m2"); } } class Util { public <T extends I1 & I2> void print(Functional1<Void, T> printer, T... array) { for (T t : array) { printer(t); } } }
これを呼び出してみます。
public void method() { Util.print( new Functional1<Void, ...>() { public Void invoke(... t) { t.m1(); t.m2(); return null; } }, new A(), new B()); }
この時、...に指定することが出来るような型は有りません。*2
なぜならば、I1を指定するとI2のメソッド(m2)は呼び出せませんし、I2を指定すれば逆の事が起きます。
AだとBのインスタンスは受け取れないし、Bにするとまた逆のことが起きます。
これを解決する方法は無くはないですが、美しくなく現実的ではありません。*3
Java8では直接指定できるようになるというわけではなく、型推論からダイアモンドオペレータを使えるので、ローカルクラスなどを用いて次のように書けます。
class Printer<T extends I1 & I2> implements Functional1<Void, T> { public Void invoke(T t) { t.m1(); t.m2(); return null; } } Util.print(new Printer<>(), new A(), new B()); // 前節の型推論の強化のおかげでダイアモンドオペレータを使うことができる
また、この場合ではラムダ式を用いて書いても問題ありません。
Util.print(t -> {t.m1();t.m2();}, new A(), new B());