Option型をmaybe風に使ってみたかった

例によって実用性皆無の実験的なコードですが……


id:j5ik2oさんの作られたOption型

Javaでも、ScalaのOption型と似て非なるOption型を作れないかなーと思い、思いつきと勢いでコード書いてみました。

は良いアイデアだと思うのだけど、メソッドチェーンのように連続してメソッドを呼び出しているような場合、結局分岐を書いたりしないといけないのが辛い。


Null Objectパターン的な発想と組み合わせて、「Noneの場合に何を呼んでもNoneを戻す」ようにできれば便利なんじゃないかと思ってやってみた。


※タイトルのmaybeというのは、「F#プログラマのためのMaybeモナド入門 - みずぴー日記」を見てインスパイアされました。

使い方

羊クラスに親羊を取得するメソッドがあったとする。

    public interface Sheep {
        /** @return この羊の母親がいれば母親を、居なければnullを戻す */
        Sheep getMother();
    }

これを使って、祖母羊を取得するユーティリティメソッドを書こうとする。

    /** 祖母を返すユーティリティメソッド ※NullPointerExceptionが発生する良くない例 */
    public static Sheep getGrandmum(Sheep sheep) {
        return sheep.getMother().getMother();
    }

これは引数のsheepがnullの時やsheepの母親がnullの時にNullPointerExceptionが発生してうまく動かない。
NullPointerExceptionが出ないようにnullチェックを入れるとこうなる。

    /** 祖母を返すユーティリティメソッド ※nullチェックをおこなう例 */
    public static Sheep getGrandmum(Sheep sheep) {
        return sheep == null
            ? null
            : sheep.getMother() == null
                ? null
                : sheep.getMother().getMother();
    }

これは面倒だ。祖母ぐらいならまだいいけど曾祖母とか曾々祖母になってくるとネストも深くなってかなりウザい。


そこで今回作ったmaybe()というメソッドを呼ぶようにする。

    /** 祖母を返すユーティリティメソッド ※maybe()を使用 */
    public static Sheep getGrandmum(Sheep sheep) {
         Option<Sheep> option = (Option<Sheep>)
             maybe(sheep, Sheep.class).getMother().getMother();

         return option.getOrElse(null);
    }

maybe()を呼ぶと、引数がOption型に変換され、しかも元と同じ型としてメソッド呼び出しでき、nullの場合はそれ以降は無条件にNoneを返すようになる。

曾々祖母を取得する時も呼び出し回数を増やすだけ。

    public static Sheep getGreatGreatGrandmum(Sheep sheep) {
        Option<Sheep> option = (Option<Sheep>)
            maybe(sheep, Sheep.class).getMother().getMother().getMother().getMother();
        return option.getOrElse(null);
   }

但し制限があって、チェーンの間中ずっと、戻り値の型はInterfaceでないといけない。
あと最後にOptionにキャストして中身を取り出さないといけないのが残念な感じ。

ソースコード

Option、Some、Noneを少し変更させてもらった。


まず、OptionインタフェースにisEmptyを追加。

package sample.option;

public interface Option<T> {
    // オプションから値を取得する。
    public T get();
    // オプションから値を取得するが、値がない場合はdefaultValueを返す。
    public T getOrElse(T defaultValue);
    // 値がない場合trueを戻す
    public boolean isEmpty();
}

SomeとNoneにisEmpty()を実装し、あとequals()で「相手がProxyの場合に相手のequals()を呼ぶ」ように変更

package sample.option;

import java.lang.reflect.Proxy;
import org.apache.commons.lang.Validate;

final class Some<T> implements Option<T> {
...
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        // Proxyの場合、左右を逆にして評価
        if (obj instanceof Proxy) {
            return obj.equals(this);
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        @SuppressWarnings("unchecked")
        Some<T> other = (Some<T>) obj;
        return value.equals(other.value);
    }
    /** この実装では常にfalseを戻す */
    public boolean isEmpty() {
        return false;
    }
package sample.option;

import java.lang.reflect.Proxy;
import java.util.NoSuchElementException;
import org.apache.commons.lang.Validate;

final class None<T> implements Option<T> {
...
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        // Proxyの場合、左右を逆にして評価
        if (obj instanceof Proxy) {
            return obj.equals(this);
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        @SuppressWarnings("unchecked")
        None<T> other = (None<T>) obj;
        return clazz.equals(other.clazz);
    }

    /** この実装では常にtrueを戻す */
    public boolean isEmpty() {
        return true;
    }

これを使ってmaybeというユーティリティメソッドを定義する。

package sample.option;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public final class Options {
    /**
     * targetをmaybe化して戻す。
     * 戻り値はTとOptionの両方にキャスト可能。
     * @param <T> targetの型
     * @param target maybe化する対象となるオブジェクト
     * @param clazz targetが実装しているインタフェース
     */
    public static <T> T maybe(final Object target, Class<T> clazz) {
        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("" + clazz.getName() + " is not an interface.");
        }
        if (target != null && target.getClass().isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("The class of target object " + target.getClass().getName() +
                            "does not implements "+ clazz.getName());
        }
        final Option<T> option = target == null ? None.of(clazz) : Some.<T>of(clazz.cast(target));

        return clazz.cast(Proxy.newProxyInstance(
            Thread.currentThread().getContextClassLoader(), // 悩ましい
            new Class[] {Option.class, clazz},
            new InvocationHandler() {
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (method.getDeclaringClass() == Object.class ||
                            method.getDeclaringClass() == Option.class) {
                        return method.invoke(option, args);
                    } else {
                        return maybe(option.isEmpty() ? null : method.invoke(option.get(), args),
                                method.getReturnType());
                    }
                }
            })
        );
    }
}

Proxyを使って、引数の型TとOptionの両方にキャスト可能なProxyを戻している。
OptionまたはObjectのメソッドが呼ばれた場合は内部で生成したOptionオブジェクト(Some or None)に委譲し、
それ以外の場合はtargetのメソッドを呼び出しつつ、戻り値をさらにmaybe化することで、メソッドチェーンに対しても継続的に適用されるようにしている。

他の使用サンプル

流れるようなインタフェースっぽいコードがあったとして、

  private void makeFluent(Customer customer) {
       customer.newOrder()
               .with(6, "TAL")
               .with(5, "HPK").skippable()
               .with(3, "LGV")
               .priorityRush();
   }

javadocみたらこんな無体なことが書いてあったら、

public static interface Order {
    /**顧客の与信額をオーバーした場合はnullを戻す。 */
    Order with(int amount, String productName);
…

としても安心。

import static sample.option.Options.maybe;
...
  private void makeFluent(Customer customer) {
        Option<Order> option = (Option<Order>) maybe(customer, Customer.class)
        .newOrder()
        .with(6, "TAL")
        .with(5, "HPK").skippable()
        .with(3, "LGV")
        .priorityRush();

        if (option.isEmpty()) {
            // エラー処理
        }
   }

引数にクラスは不要?

そういえばこんなテクあったかも。

    public static <T> T maybe(T target, T... dummy) {
        return maybe(target, (Class<T>) dummy.getClass().getComponentType());
    }

ちょっとだけ短くなる

        Option<Sheep> option = (Option<Sheep>) maybe(sheep).getMother().getMother();
        return option.getOrElse(null);

毎回Optionにキャストするのが嫌?

チェック例外使わないなら、呼び出した先でキャストして戻すことは可能。でも簡潔さが……

    public static interface MaybeClient<S, T> {
        S with(T maybeT); 
    }
    public static <S, T> Option<S> maybe(T target, MaybeClient<S, T>  client) {
        return (Option<S>) client.with(maybe(target));
    }

使う側

    public Sheep getGreatGreatGrandmum(Sheep sheep) {
        Option<Sheep> option = 
            maybe(sheep, new MaybeClient<Sheep, Sheep>() {
                    public Sheep with(Sheep sheep) {
                        return sheep.getMother().getMother().getMother().getMother();
                    }
                });
        return option.getOrElse(null);
   }