オフラインどう書く第九回の問題をCompileStatic指定してやってみた。
オフラインどう書く第九回の問題をやってみたという記事を書きましたが、今回はCompileStaticアノテーションを使って書きなおしてみました。
import groovy.transform.* @CompileStatic int ceil10(int n) { (Math.ceil(n/10) * 10) as Integer } @CompileStatic int half(int n) { ceil10(n/2) } @CompileStatic List parse(String input) { String[] tmp = input.split(':') String p = tmp[0] String list = tmp[1] return [Integer.parseInt(p), list.split(",") as List] } @CompileStatic int calc(int basePrice, List<String> passengers) { Map<String,Closure> map map = [An:{int it->it}, Ap:{int it->0}, Aw:{int it->half((int)map.get('An')(it))}, Cn:{int it->half((int)map.get('An')(it))}, Cp:{int it->0}, Cw:{int it->half((int)map.get('Cn')(it))}, In:{int it->half((int)map.get('An')(it))}, Ip:{int it->0}, Iw:{int it->half((int)map.get('In')(it))}, ] Map<String,List> groups = passengers.collect{String it -> [it, map.get(it)(basePrice)]}.groupBy{List<String> it -> it[0][0]}.withDefault{[]} return ((groups.get('A')+groups.get('C')+groups.get('I').sort{ List<Integer> it -> it[1] }.take(groups.get('I').size()-(groups.get('A').size()*2))).sum{List<Integer> it -> it[1]}) as Integer } @CompileStatic void test(String input, String expected) { List tmp = parse(input) int basePrice = (int)tmp[0] List<String> passengers = (List<String>)tmp[1] assert Integer.parseInt(expected)==calc(basePrice, passengers) } test( "1480:In,An,In,In,In,Iw,Cp,Cw,In,Aw,In,In,Iw,Cn,Aw,Iw", "5920" );
マップの要素指定がプロパティ形式でアクセスできなくなったり、クロージャの暗黙引数が省略できないケース(型推論できない場合)があったり、結構冗長になっていることがわかります。ちなみに上記の書き直しのために、staticalizerは役立ちます。
あと正しいTupleが欲しい。
本記事は、JJUG CCC 2013 Springのステマです。
オフラインどう書く第九回の問題をJava 8 Lambdaでやってみた。
という記事を書きましたが、今回はJava 8 Lambda式を使って書きなおしてみました。
import java.util.*; import java.util.function.*; import java.util.stream.*; class BusJava8 { int ceil10(int n) { return (int)(Math.ceil(n/10) * 10); } int half(int n) { return ceil10(n/2); } List parse(String input) { String[] tmp = input.split(":", 0); String p = tmp[0]; String list = tmp[1]; return Arrays.asList(Integer.parseInt(p), Arrays.asList(list.split(",", 0))); } int calc(int basePrice, List<String> passengers) { Map<String,Function<Integer,Integer>> map = new HashMap<>(); map.put("An", (Integer it)->it); map.put("Ap", (Integer it)->0); map.put("Aw", (Integer it)->half(map.get("An").apply(it))); map.put("Cn", (Integer it)->half(map.get("An").apply(it))); map.put("Cp", (Integer it)->0); map.put("Cw", (Integer it)->half(map.get("Cn").apply(it))); map.put("In", (Integer it)->half(map.get("An").apply(it))); map.put("Ip", (Integer it)->0); map.put("Iw", (Integer it)->half(map.get("In").apply(it))); Map<String,List<List>> groups =passengers.stream() .map(it -> Arrays.asList(it, map.get(it).apply(basePrice))) .collect(Collectors.groupingBy(it -> (((String)it.get(0)).substring(0,1)))); long size = (long)(groups.get("I").size()-groups.get("A").size()*2); groups.get("I").sort((a, b) -> (Integer)(a.get(1)) - (Integer)(b.get(1))); List<List> tmp = new ArrayList(groups.get("A")); tmp.addAll(groups.get("C")); tmp.addAll(groups.get("I").stream().limit(size).collect(Collectors.toList())); return tmp.stream().mapToInt(it->(Integer)it.get(1)).sum(); } static void test(String input, String answer) { Java8 b = new Java8(); List tmp = b.parse(input); int basePrice = (int)tmp.get(0); List<String> passengers = (List<String>)tmp.get(1); int expected = Integer.parseInt(answer); int result = b.calc(basePrice, passengers); assert result == expected; } public static void main(String[] args) { test("1480:In,An,In,In,In,Iw,Cp,Cw,In,Aw,In,In,Iw,Cn,Aw,Iw", "5920"); } }
上では、自分がJava8に全く不慣れなため、非効率・冗長な記述をしているところがあるかもしれません。また、そもそも元のクロージャを駆使したアルゴリズムがGroovy向きだった、など比較としてはフェアではないかもしれませんので、ご留意を。Java 8Lambdaの仕様も未確定だと思いますので、現時点でのeaでの仕様であることも注意ください。
で、感想を言うと、こりゃ書くのたいへんだわ、です。Java8の補完対応IDEを使わなかったこと、などもあると思いますが…。あと結構エラーメッセージが怖いです。たとえば
Java8.java:28: エラー: 不適合な型: 推論変数Rには、不適合な境界があります .collect(Collectors.groupingBy(it -> (((String)(it.get(0))).substring(0,1))));
^
等価制約: Map>>
上限: Map,Object
R,Tが型変数の場合:
メソッドcollect(Collector)で宣言されているRはObjectを拡張します
インタフェース Streamで宣言されているTはObjectを拡張します
INT#1,INT#2がintersection型の場合:
INT#1はObject,Serializable,Comparableを拡張します
INT#2はObject,Serializable,Comparableを拡張します
とか。型指定を1つ間違えただけで上のメッセージは…。
他に、Groovyと比べると、例えLambdaを使っても、
- オペレータオーバーロードが無い
- Collectionとstreamが別ものである
- リストやマップリテラルが無い
- 破壊的ではないリストの結合とかsortとかが無い(見付けられなかっただけかも…)ようなので、メソッドチェインが途切れてしまうのも気になります
などにより、冗長性は高くなるようです。シンプルなケースを除き、高階関数をバリバリ使って関数型ライクに書くのは余り積極的になれない気がしました。リリースまでには多くが解決されてしまうかもしれませんが*1。
Lambda式の良い点は、GroovyのCompileStticに比べても、型推論の精度が高いということ。GroovyのCompileStaticでは、クロージャの戻り値の型は推論に寄与してないようで、チェインすると2個目以降に明示指定が必要になります。
さて、速度はどうだったか、などはJJUG CCC 2013 Springのセッションにて(ステマ)。
*1:まあ、lambdaの主眼はマルチコア細粒度並列処理と言われているので、もしそうなら通常の処理にあえて使う必然性もないわけですけれども。