というわけで、通りすがりに見かけた記事ですが、Genericsの使い方について、ちょっと引っ掛かったのでコードを書いてみました。
import java.util.List; import java.util.Arrays; public class Foo { public static <T> T sum(List<T> list) { int ret = 0; for (T i : list) { ret += i; // コンパイルエラー } return ret; } public static void main(String[] args) { List<Integer> list = Arrays.asList(5,2,3,1,4); System.out.println(sum<Integer>(list)); } }
元のコードがこんな感じで、コンパイルエラーなんですけど、対処方法として、謎のインターフェースを定義して、型キャストとかも発生していたので、こんな感じにしたほうが良いよ?と言いたかっただけです。
import java.util.List; import java.util.Arrays; interface Addable<T> { T add(T a, T b); T zero(); } public class App { public static <T> T sum(List<T> list, Addable<T> adda) { T ret = adda.zero(); for (T i : list) { ret = adda.add(ret, i); } return ret; } public static void main(String[] args) { List<Integer> list = Arrays.asList(5,2,3,1,4); System.out.println(sum(list, new Addable<Integer>() { public Integer add(Integer a, Integer b) { return a + b; } public Integer zero() {return 0;} })); } }
まあ、関数のシグネチャーが変わってしまっているのですが、「加算」を抽象化したインターフェースを定義して、そいつを渡してあげると型キャストも発生せずにコード量も増えすぎず、Genericsな感じで拡張できます。
Doubleの加算をしたいときも、上記で匿名クラスを渡している部分を下記のようにするだけです。
new Addable<Double>() { public Double add(Double a, Double b) { return a + b; } public Double zero() { return 0.0; } }
まあ、これってもろモノイドなんですけど・・・。
ちょっと本質的な話
コンビニにおやつを買いに歩いてたら、ちょっと考えがまとまったので追記します。
元記事のコードと僕の挙げたコードの違いは何でしょうか?
元記事では、「Number」や「Int」といった「データ」を抽象化していました。
それに対して、僕のコードでは、「加算」という「計算」を抽象化しています。
「計算」を抽象化するという概念は、関数型言語ではオーソドックスなスタイルであり、そのために型クラスという概念が日常的に利用され、アドホック多相を実現しています。
これに対して、オブジェクト指向では、「対象」を抽象化するというアプローチが一般的に採られ、「データ」を抽象化する傾向にあります。
このアプローチは、処理をうまく抽象化できず、歪を生みます。
歪を正すために更に大きな歪みを作り続ける袋小路に陥ることもよくあります。
僕自身、関数型言語の考え方を学ぶ前は、オブ脳的な考えで、設計や実装を行っていました。
その頃は、「なぜ処理が冗長になってしまっているのか?」理解できませんでした。むしろ、「これが限界なんだ」と自分に言い聞かせるようにコードを書いていたのかもしれません。
関数型プログラミング賛辞のような文章になってしまいましたが、「万能ですごい!」と言うつもりもないですし、僕自身関数型プログラミングに対してそんな感情を持ちません。
ただ、両方のアプローチを理解することで、確実にコンピューティングが豊かになることは間違いないと思います。