■ Maybe の定義
いろんなアプリカティブファンクターがあるけど、とりあえず簡単な Maybeで やってみる。
こんなふうに定義してみた。
abstract class Maybe<A> { abstract boolean isNothing(); public A just() { throw new AssertionError(); } public static <A> Maybe<A> just(A a) { return new Maybe<A>() { public boolean isNothing() { return false; } public A just() { return a; } public String toString() { return format("Just(%s)", a); } }; } private static final Maybe<?> NOTHING = new Maybe() { public boolean isNothing() { return true; } public String toString() { return "Nothing"; } }; public static <A> Maybe<A> nothing() { @SuppressWarnings("unchecked") Maybe<A> result = (Maybe<A>) NOTHING; return result; } }まあ Java 8 の Optional が、実質 Maybe なんだけど、ここでは後の作業の見通しを良くするためと、Haskellっぽい識別子にするために、あえて定義しなおした。簡単だし。
■ Applicative Maybe の定義
次に Applicative Maybe の定義だけど、前回の投稿で Functor を interface として定義したので、それを extends する形で書いてみたくもなるが、ちょっとやってみたら余りうまく行かなそうな予感がしたので、ここでは ApplicativeFunctor をユーティリティクラスとして定義し、クラスメソッドで pure と <*> を実装することにした。
class MaybeApplicative<A, B> { // fmap :: (a -> b) -> f a -> f b public static <A, B> Function<Maybe<A>, Maybe<B>> fmap(Function<A, B> f) { return maybe -> maybe.isNothing() ? nothing() : just(f.apply(maybe.just())); } // pure :: a -> f a public static <T> Maybe<T> pure(T t) { return just(t); } // (<*>) :: f (a -> b) -> f a -> f b public static <A, B> Function<Maybe<A>, Maybe<B>> amap(Maybe<Function<A, B>> mf) { return mf.isNothing() ? ma -> nothing() : ma -> ma.isNothing() ? nothing() : just(mf.just().apply(ma.just())); } }
- 「ApplicativeFunctor ならば Functor でもある」ので、fmap も定義。
- <*> みたいな、かっちょいい記号が使えないので、amap() ってメソッドにした。
Maybe<String> m1 = just("m1"); Maybe<String> m2 = nothing(); Maybe<Function<String, Integer>> mf1 = just(String::length); Maybe<Function<String, Integer>> mf2 = nothing(); println(amap(mf1).apply(m1));// Just(2) println(amap(mf1).apply(m2));// Nothing println(amap(mf2).apply(m1));// Nothing println(amap(mf2).apply(m2));// Nothing
■ アプリカティブ則
以下、4つのアプリカティブ則と、fmap に関連するもう一個のルールを試してみる。というか、これを確かめるコードを Java で書いたらどんな感じになるかやってみる。
pure id <*> v = v -- Identity pure (.) <*> u <*> v <*> w = u <*> (v <*> w) -- Composition pure f <*> pure x = pure (f x) -- Homomorphism u <*> pure y = pure ($ y) <*> u -- Interchange fmap f x = pure f <*> x -- Fmap● Identity
// pure id <*> v = v static void checkIdentity(Maybe<String> v) { Function<String, String> id = s -> s; Object o1 = amap(pure(id)).apply(v); printAssertion(o1, v); }こんな感じになる。
checkIdentity(just("hello")); // Just(hello) == Just(hello) checkIdentity(nothing()); // Nothing == Nothingちなみに printAssertion()は以下のようなヘルパ。敢えて標準出力目視確認。
static void printAssertion(Object o1, Object o2) { println(format("%s == %s", o1, o2)); } static void println(Object obj) { System.out.println(obj.toString()); }● Composition
// pure (.) <*> u <*> v <*> w = u <*> (v <*> w) static void checkComposition( Maybe<Function<Integer, String>> u, Maybe<Function<String, Integer>> v, Maybe<String> w) { Function< Function<Integer, String>, Function<Function<String, Integer>, Function<String, String>>> compose = f -> g -> f.compose(g); Object o1 = amap(amap(amap(pure(compose)).apply(u)).apply(v)).apply(w); Object o2 = amap(u).apply(amap(v).apply(w)); printAssertion(o1, o2); }Function::compose() をカリー化して、パラメータを一個一個適用しているので大分ややこしくなったが、実際どんな計算になっているか、かえってはっきり分かるようになった。
以下のように動かしてみたが、just と nothing の組み合わせを網羅しきってはいないけど(面倒くさくなってきたので)、大体あってる感じだ。
checkComposition( // Just(7) == Just(7) just(Object::toString), just(String::length), just("1234567")); checkComposition( // Nothing == Nothing nothing(), just(String::length), just("1234567")); checkComposition( // Nothing == Nothing just(Object::toString), nothing(), just("1234567")); checkComposition( // Nothing == Nothing just(Object::toString), just(String::length), nothing()); checkComposition( // Nothing == Nothing nothing(), nothing(), nothing());● Homomorphism
// pure f <*> pure x = pure (f x) static void checkHomomorphism(Function<String, Integer> f, String x) { Object o1 = amap(pure(f)).apply(pure(x)); Object o2 = pure(f.apply(x)); printAssertion(o1, o2); }
// Just(6) == Just(6) checkHomomorphism(String::length, "123456");● Interchange
// u <*> pure y = pure ($ y) <*> u static void checkInterchange( Maybe<Function<String, Integer>> u, String y) { Function<Function<String, Integer>, Integer> $y = f -> f.apply(y); Object o1 = amap(u).apply(pure(y)); Object o2 = amap(pure($y)).apply(u); printAssertion(o1, o2); }
// Just(5) == Just(5) checkInterchange(just(String::length), "12345"); // Nothing == Nothing checkInterchange(nothing(), "12345");● Fmap
// fmap f x = pure f <*> x static void checkFmap(Function<String, Integer> f, Maybe<String> x) { Object o1 = fmap(f).apply(x); Object o2 = amap(pure(f)).apply(x); printAssertion(o1, o2); }
// Just(4) == Just(4) checkFmap(String::length, just("1234")); // Nothing == Nothing checkFmap(String::length, nothing());疲れた・・・
■ リフト関数 liftA2
ついでに Haskell の Control.Applicative にある liftA2 も、amap() を使って実装してみる。
Prelude Control.Applicative> :t liftA2 liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c以下のメソッドを ApplicativeFunctor クラスに追加した。
public static <A, B> Maybe<B> amap(Maybe<Function<A, B>> mf, Maybe<A> a) { return amap(mf).apply(a); } public static <A, B, C> Function<Maybe<A>, Function<Maybe<B>, Maybe<C>>> liftA2(Maybe<Function<A, Function<B, C>>> mf) { return a -> b -> amap(amap(mf, a), b); }この liftA2 もカリー化した関数を返すようにしてみた (新しく追加した amap(Maybe, Maybe) は apply() 呼び出しの重複を取り除くため)。
以下のように使える。
Function<String, Function<Integer, Character>> charAt = s -> index -> s.charAt(index); // Just(d) println(liftA2(pure(charAt)).apply(just("abcde")).apply(just(3)));関数と2つのパラメータを同時に受け取るシグネーチャでも書けるけど、自明なので省略。
あと、Java 8 には BiFunction ってのもあるので、これを用いた liftA2 も書いてみた。
public static <A, B, C> BiFunction<Maybe<A>, Maybe<B>, Maybe<C>> liftBiFunctionA2(Maybe<BiFunction<A, B, C>> mbf) { Maybe<Function<A, Function<B, C>>> mf = mbf.isNothing() ? nothing() : just( a -> b -> mbf.just().apply(a,b)); return (a, b) -> liftA2(mf).apply(a).apply(b); }こうなる。
BiFunction<String, Integer, Character> charAt2 = (s, index) -> s.charAt(index); // Just(a) println(liftBiFunctionA2(pure(charAt2)).apply(just("abcde"), just(0)));====
しかし、Haskell みたいに<*> とか <$> とか使えたら、いかにもアプリカティブ・スタイルって感じでかっちょいいんだけど、Java だときついな。
まあ、あえて無理やりやってみることで、いろいろ腑に落ちたり、体感できたりする事もあるので良しとしよう。
0 件のコメント:
コメントを投稿