2014年3月23日日曜日

Java8 の Optionalを Haskellの Maybeモナドっぽく使ってみる

Haskell wiki のこのページに Maybe モナドを使ったサンプルがある。

Java 8 では Maybe にあたる Optional というクラスがあるので、これを用いてモナドっぽくコーディングしてみる。

■ お題の説明

以下のサンプル Haskell コードを実装する。
father :: Person -> Maybe Person
mother :: Person -> Maybe Person

bothGrandfathers p =
    father p >>=
        (\f -> father f >>=
             (\gf -> mother p >>=
                  (\m -> father m >>=
                       (\gm -> return (gf,gm) ))))
father と mother は、架空のデータベースから与えられた人物の父/母を検索し、該当があれば Just でくるんで、なければ Nothing を返す関数。

bothGrandfathers は、与えられた人物の祖父を検索し、両祖父が登録されていたらそれを Just で、なければ Nothing を返す。

Haskell ではさらに do 構文を用いて、簡潔なコードにできるが、Java では無理なので、ここまでをお題としてみたい。

■ サンプルデータの説明

とりあえず、聖徳太子とその尊属を部分的に用いた、下記のようなデータでやってみる。
継体天皇
   ├─── 欽明天皇
         ├─── 用明天皇
蘇我稲目    │      │
   │  ┌ 蘇我堅塩媛   ├──── 聖徳太子      
   ├──┤        │
      └ 蘇我小姉君    │
          ├─── 穴穂部間人皇女
        (欽明天皇)
この中で両祖父が共に登録されているのは用明天皇と聖徳太子だけなので、彼らの両祖父が Just で取れて、他は Nothing になればいい。

■ 擬似データベース

以下のような擬似データベースを書いてみた。もとの Haskell コードの father / mother は、このクラスの fatherOf(), motherOf() として実装した。

ここでは、せっかくの Java 8 ということで、EDSL への応用をちょっと試してみた。
class ParentChildDatabase {
  static class Relation {
    private final String child;
    private final ParentType type;

    Relation(String child, ParentType type) {...略...}
    public boolean equals(Object obj) {...略...}
    public int hashCode() {...略...}
  }
  private static final Map<Relation, String> database = new HashMap<>();

  private static void put(String child, ParentType type, String parent) {
     database.put(new Relation(child, type), parent);
  }
  static interface Register { As   as(ParentType type); }
  static interface As       { void of(String child); }
  static Register register(String parent) {
    return type -> child -> put(child, type, parent);
  }

  private static Optional<String> get(String child, ParentType type) {
    final Relation key = new Relation(child, type);
    return database.containsKey(key) ? Optional.of(database.get(key))
                                     : Optional.empty();
  }
  static Optional<String> fatherOf(String child) {
    return get(child, ParentType.FATHER);
  }
  static Optional<String> motherOf(String child) {
    return get(child, ParentType.MOTHER);
  }
}
Register と As は関数インターフェイスで、register() と共に用いて、EDSL を形成するためのもの。

等価なコードを仮に Java 8 の標準APIで書くとしたら、

Function<ParentType, Consumer<String>> register(String parent)

みたいなシグネーチャになる。

以下のように使う。
  register("用明天皇").as(FATHER).of("聖徳太子");
  register("穴穂部間人皇女").as(MOTHER).of("聖徳太子");

  register("欽明天皇").as(FATHER).of("用明天皇");
  register("蘇我堅塩媛").as(MOTHER).of("用明天皇");

  register("欽明天皇").as(FATHER).of("穴穂部間人皇女");
  register("蘇我小姉君").as(MOTHER).of("穴穂部間人皇女");

  register("継体天皇").as(FATHER).of("欽明天皇");
  register("蘇我稲目").as(FATHER).of("蘇我堅塩媛");
上記のように、register が 入れ子になった 関数インターフェイス を返すので、一個ずつ適用していく。

ちなみに、この例とはちょっと違うが、同じ要領で応用すれば、名前付きパラメータみたいことも実装できるだろう(順序は固定だけど)。

■ bothGrandfathers の実装

Haskell のモナドの >>=、いわゆる bind は、Java 8 の Optional では flatMap() が、どうやらこれに相当する。

ちなみに Haskell の fmap は、Optional の map() に対応しているように見え、さらに、Stream にも同じようなシグネーチャの対応がみられる。
↓ Haskell の fmap と bind
fmap   :: (a -> b) -> f a -> f b
(>>=)  :: m a -> (a -> m b) -> m b
↓ Java 8 の Optional と Stream の、それぞれの map と flatMap
// Optional の map と flatMap
interface Optional<T>
  Optional<U>  map(Function<T, U> mapper)
  Optional<U>  flatMap(Function< T,Optional<U>> mapper)

// Stream の map と flatMap
interface Stream<T> 
  Stream<R>  map(Function<T, R> mapper)
  Stream<R>  flatMap(Function<T, Stream<R>> mapper)
というわけで、サンプルの Haskell コード の (>>=) 部分を flatMap を用いて以下のように書き換えてみた(もとの Haskell コードでは変数名が省略されていてさすがに分かりにくかったので、ここでは英単語を省略せずに使った)。
  private static Optional<List<String>> bothGrandfathers(String person) {
    return fatherOf(person).flatMap(
             father -> fatherOf(father).flatMap(
               fatherOfFather -> motherOf(person).flatMap(
                 mother -> fatherOf(mother).flatMap(
                   fatherOfMother -> Optional.of(Arrays.asList(
                       new String[] {fatherOfFather, fatherOfMother}))))));
  }
割と Haskell のサンプルコードと似た感じにできたんじゃないかと思う。

で、こんな感じで動かすと,,,
    System.out.println(bothGrandfathers("聖徳太子"));
    System.out.println(bothGrandfathers("用明天皇"));
    System.out.println(bothGrandfathers("欽明天皇"));
こんな出力が得られる
     [java] Optional[[欽明天皇, 欽明天皇]]
     [java] Optional[[継体天皇, 蘇我稲目]]
     [java] Optional.empty
====
聖徳太子って両祖父が同じなんだね。へえ。

0 件のコメント:

コメントを投稿