2014年3月23日日曜日

Java 8 で アプリカティブファンクターを考えてみる

この Haskell の wikiページの解説を元に、Java 8 でアプリカティブを書いてみる。

■ 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 件のコメント:

コメントを投稿