結論を先に言えば、ちょっとシンドい。
やってみただけ。特にお勧めはしません。
お勧めしない理由は、表記が煩雑なので、書くのも読むのもバカバカしくめんどくさいからです。内容的には“多相の型付きラムダ計算”になります。だったら、なにもわざわざJavaで書かなくても…;まー、そうなんですが、僕みたいな年寄りからすると、産業界でメジャーな言語のひとつで、こんなお遊びができることが感慨深いわけです。
それはそうとして、このテの内容の記述に関して、Javaがいかに不向きかというと……(先を読んでください。)
課題とその説明
fが集合Xから集合Yへの関数であることを、f:X→Y と書きます。f:X→Y、g:Y→Zに対して、h(x) = g(f(x)) として定義される関数h:X→Zを、fとgの結合(composition; 合成)と呼び、f;g とか g・f とかで表します(fとgの順序に注意)。
さて、この結合の操作を再び関数と考えれば、comp:((X→Y), (Y→Z))→(X→Z) ですよね。えっ? わけわからない?; (X→Y) は、「集合Xから集合Yへの関数の集合」と読んでください。つまり、f:X→Y は f∈(X→Y)と同じことです。h = comp(f, g) とすれば、compは、関数f∈(X→Y)と関数g∈(Y→Z)を2つの引数に取って、関数h∈(X→Z)を返すでしょう。よって、compは、2つの集合(X→Y)と(Y→Z)の組から集合(X→Z)への関数となり、comp:((X→Y), (Y→Z))→(X→Z) という書き方が合理化されます。
compは2引数の関数ですが、カリー化したcomp'は、comp':(X→Y)→((Y→Z)→(X→Z)) となります。f:(X, Y)→Z のカリー化が f':X→(Y→Z) であることを思い出せばわかるでしょ。でも今回は、2引数のままでもカリー化でもどっちでもいい、気にしないことにします。
さて課題; 以上に定義したcompをプログラミング言語でちゃんと定義してください。
関数型言語でやってみる
手元にmosml(Moscow ML)ってML処理系があるので、これでやってみます(最近はHaskellのほうが流行?)。
- fun comp f g = fn x => g (f x);
> val ('a, 'b, 'c) comp = fn : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c
- fun square n = n * n;
> val square = fn : int -> int
- val result = (comp size square) "hello";
> val result = 25 : int
- result;
> val it = 25 : int
ゴチャゴチャしてますが、「-」の後が人間(僕)がタイプした部分で、「>」の後は処理系からの応答です。プログラムだけをファイルに書けば定義が1行、利用例入れて実質3行ってこと。
fun comp f g = fn x => g (f x) (* 関数compの定義 *) fun square n = n * n (* 関数squareの定義 *) (* 文字列の長さを求めるsizeは組み込み関数 *) val result = (comp size square) "hello"
あっ、そうそう、対話的実行の2行目の('a -> 'b) -> ('b -> 'c) -> 'a -> 'c に注目、'a、'b、'cをX, Y, Zに書き換えて、省略してある括弧を補うと、(X -> Y) -> ((Y -> Z) -> (X -> Z))、これがcompの型だって言っているわけ -- 課題説明のなかで言及したカリー化したcompの型そのものでしょう。人間が定義(comp := λf.λg.(λx.g(fx)) に相当)を入力しただけで、compの型(高階で総称的な型)を推論してくれます。
Javaでやってみる
「Javaで高階プログラミング、ってそれ何よ!」で紹介した方法でcompを定義してみます。Function<X, Y>が、(X→Y)という関数の型、Function2<X, Y, Z>なら、((X, Y)→Z)という2引数の関数の型です。
まず、square(整数を平方する関数)とsize(文字列の長さを返す関数)を定義しておきます。
class Square implements Function<Integer, Integer> { public Integer apply(Integer n) { return n * n; } } class Size implements Function<String, Integer> { public Integer apply(String s) { return s.length(); } }
それで、compのもとになるクラスCompose; 下にあるmainは、MLのときと同じ利用例。
public class Compose<X, Y, Z> implements Function2<Function<X, Y>, Function<Y, Z>, Function<X, Z>> { public Function<X, Z> apply(final Function<X, Y> f, final Function<Y, Z> g) { return new Function<X, Z>() { public Z apply(X x) { return g.apply(f.apply(x)); } }; } // for demo public static void main(String[] args) { Function<String, Integer> size = new Size(); Function<Integer, Integer> square = new Square(); Function2<Function<String, Integer>, Function<Integer, Integer>, Function<String, Integer>> comp = new Compose<String, Integer, Integer>(); int result = (comp.apply(size, square)).apply("hello"); System.out.println("result = " + result); } }
C:\tmp>java Compose result = 25
総括
MLやHaskellの定義はともかく短い:
ML:
fun comp f g = fn x => g (f x);
Haskell:
comp f g = \x -> g (f x)
あんまり短くてわかんないのでJavaScript:
function comp(f, g) { return function (x) { return g(f(x)); }; }
しかしJavaScriptでは、fとgが結合可能な関数であることは保証されないし、結果の関数がどんな型かの情報もありません。
Java版は、fやgの型を手動で明示的に付けています。JavaScript版に手動の型指定を入れれば、こんな感じか。
function comp<X, Y, Z>(f:(X->Y), g:(Y->Z)):(X->Z) { return function (x:X) { return g(f(x)); }; }
compを使うときは、X, Y, Zを埋めなくてはならないから:
comp<String, Number, Number>(size, square)("hello")
これじゃ面倒だから、引数型からの型推論を指示するのに、
function<X, Y, Z> comp(f:(X->Y), g:(Y->Z)):(X->Z) {/*...*/}
って構文かな。
あれれ、話が横にそれた。結局、Java Genericsで書くのは得策ではないね。向き不向きがあるから、この課題で比較するのは可哀想だったとは思います>Java。