このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

「Java vs. 関数型言語」って比較は酷だ

「Javaで高階プログラミング、ってそれ何よ!」にて:

結論を先に言えば、ちょっとシンドい



やってみただけ。特にお勧めはしません。

お勧めしない理由は、表記が煩雑なので、書くのも読むのもバカバカしくめんどくさいからです。内容的には“多相の型付きラムダ計算”になります。だったら、なにもわざわざ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