■ まずは Treeから
もともとの Haskellコードが以下。
data Tree a = Leaf a | Branch (Tree a) (Tree a) deriving (Show)Java で書き直したコードが以下。
abstract class Tree<A> { abstract boolean isLeaf(); public A leaf() { throw new AssertionError(); } public Tree<A> left() { throw new AssertionError(); } public Tree<A> right() { throw new AssertionError(); } public static <A> Tree<A> of(A a) { return new Tree<A>() { public boolean isLeaf() { return true; } public A leaf() { return a; } public String toString() { return a.toString(); } }; } public static <A> Tree<A> of(Tree<A> left, Tree<A> right) { return new Tree<A>() { public boolean isLeaf() { return false; } public Tree<A> left() { return left; } public Tree<A> right() { return right; } public String toString() { return String.format("(%s, %s)", left, right); } }; } }うーん、長い。Haskell で1行なのが Javaだとたくさん書かなきゃならないが、まあ簡単っちゃ簡単。てか、引数の final いらなくなったんだね。
試してみるとこうなる。
Tree<Integer> intTree = Tree.of( Tree.of( Tree.of(1), Tree.of(2)), Tree.of(3)); System.out.println(intTree); // ((1, 2) ,3)
■ 次に fmap
ようするに (a -> b) -> f a -> f b という型のメソッドを Java で表現できればよくて、素朴でシンプルな実装は、f a を自オブジェクト this とし、(a -> b)を引数で取る形でこうなる。
abstract class Tree<A> { ... public <B> Tree<B> fmap(Function<A, B> f) { return isLeaf() ? Tree.of(f.apply(leaf())) : Tree.of(left().fmap(f), right().fmap(f)); } ...上で使った intTree に適当な関数を適用してみると、一応期待通り動く。
System.out.println( intTree.fmap(n -> "S" + n.toString())); // ((S1, S2) ,S3)
■ あと Functor
ところで fmap は Haskell では Functor として定義されていて、こんな感じになってる。
class Functor f where fmap :: (a -> b) -> f a -> f bで、Tree の fmap はこんな感じ。
instance Functor Tree where fmap f (Leaf x) = Leaf (f x) fmap f (Branch left right) = Branch (fmap f left) (fmap f right)これも一応 Java で書いてみよう(以下、ファンクター則とかとりあえず置いとく)。
まず Functor。
interface Functor<A, B, FA, FB> { Function<FA, FB> fmap(Function<A, B> f); }ここでは (a -> b) -> f a -> f b を (a -> b) -> (f a -> f b) と解釈して(Haskellでは普通だけど)、関数を返すようなシグネーチャにした。
ちなみに Java だと以下のように書けないので嫌な感じだが、まあしょうがない。
interface Functor<A, B, F> {// こうは書けない Function<F<A>, F<B>> fmap(Function<A, B> f); }次に Tree の Functor。
class TreeFunctor<A, B> implements Functor<A, B, Tree<A>, Tree<B>> { static <A, B> Tree<B> fmap(Function<A, B> f, Tree<A> a) { return a.isLeaf() ? Tree.of(f.apply(a.leaf())) : Tree.of(fmap(f, a.left()), fmap(f, a.right())); } public Function<Tree<A>, Tree<B>> fmap(Function<A, B> f) { return a -> fmap(f, a); } }fmap(Function) が本体というか、Functor インターフェイスの実装で、fmap(Function, Tree) はヘルパ用の再帰関数。
Tree に fmap を書いたやつよりも大分コードが増えたけど、Tree の責務として fmap を持つのが妥当かというとかなり疑問なので、別にしておくのが正しい気がする。
この時点では fmap を Tree から切り離して Functor に切り出したことで複雑度が増しているけど、たいていの場合、開発が進むに連れて結局は逆転して、小さくてたくさんのクラスを書くアプローチの方が楽になってくる。
動かすとこんな感じになる。
TreeFunctor<Integer, String> tf = new TreeFunctor<Integer, String>(); // ((S1, S2), S3) println(tf.fmap(f).apply(intTree)); // 部分適用 Function<Tree<Integer>, Tree<String>> f2 = tf.fmap(n -> "No." + n.toString()); // (No.1000) println(f2.apply(Tree.of(1000))); // (No.5, No.6) println(f2.apply(Tree.of(Tree.of(5), Tree.of(6))));直接 new TreeFunctor() でインスタンスを作らずに、どっか他の所にファクトリメソッドを作ったりすると、レガシーコード改善ガイドでいうSeamができてテスタビリティが上がったりするが、ここでは省略した。
====
しかし、SE 8 で若干マシになったものの、Java という言語はちょっとの事をやるのにも書かなきゃならないコードが多い。とはいえ最近では、こういう言語でいろいろ工夫するのも、割と面白いんじゃないかとも思える心境になってきていたりする。
0 件のコメント:
コメントを投稿