「Haskell でリストに対する関数を考えるときの視点」 のつづき
Haskell のリストに対応した Java クラス
前回は Haskell のリストに対応したクラスを定義し、リストの大きさを返すメソッド length を実装した。
このクラスを使い、map, filter, foldr, foldl の実装を考える。
ここで予め Main クラスで利用するリストを作成しておく。以降この変数 list を使う。
IList<Integer> list = new Nil<Integer>().cons(4).cons(3).cons(2).cons(1);
System.out.println(list); // 1 2 3 4
累積変数を使った length メソッド
まずは、前回実装した length 関数を、末尾再帰で定義したときのような格好の実装を追加してみる。
Cons クラス...
public int length(int acc) {
return this.xs.length(acc + 1);
}
累積変数 acc を導入し、そこへ要素の数を累積させる。
リストの末尾の Nil クラスでは、累積変数を返す。
public int length(int acc) {
return acc;
}
Main クラスでこれを使ってみる。
System.out.println(list.length(0)); // 4
map
次に、リストの要素に対して関数を適用する map 関数に相当するメソッドを定義する。
Haskell での map 関数の型は、
map :: (a -> b) -> [a] -> [b]
第1引数は、要素を別の型の値へ変換する関数。
Java では 「関数」 を関数の引数へ渡すことができないので、オブジェクトを代わりに渡し Strategy パターンで対応。オブジェクトの実装するインターフェイスを IUnaryOp として以下のように宣言し、利用するときはインターフェイスを実装した匿名クラスを作成する。
public interface IUnaryOp<A,B> {
public B apply(A a);
}
apply 関数は、引数に A 型の値を与えると B 型の値が返ってくる。これは Haskell の map 関数の第1引数 (a –> b) に相当。
IList インターフェイスに map メソッドの宣言を追加。
public <B> IList<B> map(IUnaryOp<A, B> f);
上記の型変数 B は、このインターフェイスを実装したクラスを生成するときではなく、このメソッドを使うときに決まるので、メソッドで型変数を宣言する。 ( cf. Java の型変数 とイレイジャ )
上記に伴ない Cons クラスで map メソッドを実装。
public <B> IList<B> map(IUnaryOp<A, B> f) {
return this.xs.map(f).cons(f.apply(this.x));
}
Nil クラスにも map メソッドを実装。
public <B> IList<B> map(IUnaryOp<A, B> f) {
return new Nil<B>();
}
これを Main クラスで使用する。
// map : 要素を 2 倍する
System.out.println(list.map(
new IUnaryOp<Integer, Integer>() {
public Integer apply(Integer a) {
return a * 2;
}
})); // 2 4 6 8
filter
filter 関数は述語を与え、リストから要素を抽出する関数。
Haskell の filter 関数の型は、
filter :: (a -> Bool) -> [a] -> [a]
第1引数は、ある型の値を渡すと Bool 型を返す述語。
先ほどの map メソッドのように、filter 関数の第1引数に相当する述語に対応したメソッドを持つオブジェクトのインターフェイスを IPredicate とし、メソッドを宣言。
public interface IPredicate<A> {
public boolean apply(A a);
}
apply メソッドは、引数に型 A の値を与えると boolean が返る。これは Haskell の filter 関数の第1引数 (a –> Bool) に相当する。
IList インターフェイスに filter メソッドの宣言を追加。
public IList<A> filter(IPredicate<A> p);
上記に伴ない Cons クラスで filter メソッドを実装。
public IList<A> filter(IPredicate<A> p) {
return p.apply(this.x) == true
? this.xs.filter(p).cons(this.x)
: this.xs.filter(p);
}
Nil クラスにも filter メソッドを実装。
public IList<A> filter(IPredicate<A> p) {
return new Nil<A>();
}
Main クラスでこれを使う。
// filter : 偶数を抽出
System.out.println(list.filter(new IPredicate<Integer>() {
public boolean apply(Integer p) {
return p % 2 == 1;
}
})); // 1 3
foldr
forldr はリストの右側から二項演算子により特定の型の値に畳み込む関数。
Haskell の foldr の型は、
foldr :: (a -> b -> b) -> b -> [a] -> b
これまでと同様、foldr の第1引数の関数に相当するメソッドを持つオブジェクトを生成するためにインターフェイス IBinaryOp1 を宣言。今回は二項演算子なので、引数が一つ増える。
public interface IBinaryOp1<A, B> {
public B apply(A a, B b);
}
apply メソッドの引数には型 A の値と型 B の値を与えると型 B の値が返る。これは Haskell の foldr 関数の第1引数の型 (a –> b –> b) に相当する。
IList インターフェイスに foldr メソッドの宣言を追加。このとき Haskell の foldr の型と比較しながら宣言を考えるとよい。
public <B> B foldr(IBinaryOp1<A, B> f, B z);
上記に伴ない Cons クラスに foldr メソッドを実装。
public <B> B foldr(IBinaryOp1<A, B> f, B z) {
return f.apply(this.x, this.xs.foldr(f, z));
}
Nil クラスにも foldr メソッドを実装。
public <B> B foldr(IBinaryOp1<A, B> f, B z) {
return z;
}
これを Main クラスで使う。
// foldr : 要素を足し合わせる
System.out.println(list.foldr(new IBinaryOp1<Integer, Integer>() {
public Integer apply(Integer a, Integer b) {
return a + b;
}
}, 0)); // 10
// foldr : リストのコピー
System.out.println(list.foldr(
new IBinaryOp1<Integer, IList>() {
public IList apply(Integer a, IList b) {
return b.cons(a);
}
},
new Nil())); // 1 2 3 4
foldl
foldr が右から畳み込むのに対して、foldl は左から畳み込む関数。
Haskell の foldl 関数の型は、
foldl :: (a -> b -> a) -> a -> [b] –> a
よく見ると foldr と型が少し違うのがわかる。
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr のときと同様に、第1引数の関数に相当するメソッドを持つオブジェクトのインターフェイスを IBinaryOp2 とする。
public interface IBinaryOp2<A, B> {
public A apply(A a, B b);
}
apply メソッドは引数型 A の値と型 B の値を与えると、型 A の値が返る。これは Haskell の foldl の第1引数 (a –> b –> a) に相当する。
IList インターフェイスに foldr メソッドを追加。ただし、foldr のときのように Haskell の型と完全に同じようになっていないことに注意。理由は、Cons クラスで実装したところを見るとわかる。
public <B> B foldl(IBinaryOp2<B, A> f, B z);
上記に伴ない Cons クラスに foldr メソッドを実装。
public <B> B foldl(IBinaryOp2<B, A> f, B z) {
return this.xs.foldl(f, f.apply(z, this.x));
}
特に第1引数が
IBinaryOp2<A, B>
ではなく、
IBinaryOp2<B, A>
であることに気をつける。
後者の場合、IBinaryOp2 インターフェイスに照らし合わせると、メソッド apply の型は次のようになる。
public B apply(B b, A a)
このように型変数名が変わっても本質的な意味は同じ。
しかし、これを Cons クラスのメソッドのシグニチャに用いると意味がそれぞれ異なる。理由は、Cons クラスが持つ型変数が A であり、この A と上記 apply メソッドの型変数名 A が一致するので、これによりメソッド全体において型変数 A の意味が定まるから。 (多分… ^^; 色々と試して動いたのがこれだったので、後付けで理由を考えた。)
Nil クラスにも foldr メソッドを定義。
public <B> B foldl(IBinaryOp2<B, A> f, B z) {
return z;
}
これを使ってみる。
// foldl : 要素を足し合わせる
System.out.println(list.foldl(
new IBinaryOp2<Integer, Integer>() {
public Integer apply(Integer a, Integer b) {
return a + b;
}
},
0)); // 10
// foldl : 要素を掛け合せる
System.out.println(list.foldl(
new IBinaryOp2<Integer, Integer>() {
public Integer apply(Integer a, Integer b) {
return a * b;
}
},
1)); // 2
メソッドチェーン
最後に上記 map, filter, foldr でメソッドチェーン。
// メソッドチェーン : filter => map => foldr :
System.out.println(list.filter(new IPredicate<Integer>() {
public boolean apply(Integer p) {
return p % 2 == 0; // 偶数を抽出
}
}).map(new IUnaryOp<Integer, Integer>() {
public Integer apply(Integer a) {
return a * 3; // 3 をかける
}
}).foldr(new IBinaryOp1<Integer, Integer>() {
public Integer apply(Integer a, Integer b) {
return a * b; // 掛け合わせる
}
}, 1)); // 72
結論は、こんなゴチャゴチャしたコードは書きたくないということで。(+_+)
型を書くのが面倒なので型推論してくれぇ~。
てゆうか、これからの実用所はやっぱ Scala っすか ?
ここまでのコード
クラス図。(メソッドにおける型変数を無理矢理書いてしまったけれど。)
関連記事