これはモナドですか?
はい、只のメソッドチェーンです。
前回のはあんまりに簡単に済ませすぎたので、もう少し掘り下げてみる。
前回のコードの振り返り
モナドっぽいところがないかといえば、この辺がそうかも。
unit(or return)っぽい部分:
final Option<T> option = target == null ? None.of(clazz) : Some.<T>of(clazz.cast(target));
bindっぽい部分:
return maybe(option.isEmpty() ? null : method.invoke(option.get(), args), method.getReturnType());
これを切り出してみる。
今回は関数的なものが使いたいので、guava-librariesのRelease 0.8に含まれるcom.google.common.base.Functionというやつ*1 を使う。これを使ってOption
unitはTを受け取ってOption
package sample.option; public final class Options { /** TからOption<T>を作る */ public static <T> Option<T> unit(T element) { return element == null ? None.<T>of() : Some.of(element); } }
なお、Noneにクラスを持たせるのは記述が煩雑になるので今回カットしています。(None.of()で作成。)
bindは「Option<T>型のオブジェクト」および「Tを受け取ってOption<S>を戻すFunction」を受け取って「Option<S>のオブジェクト」を戻すのが本来の形だけど、折角オブジェクト指向言語なので引数で渡す代わりにOption<T>にインスタンスメソッドの形で実装することにした。*2
といってもインタフェースには実装を書けないので、SomeとNoneの抽象スーパークラスとしてAbstractOption<T>を作成してそこに実装する。
package sample.option; import com.google.common.base.Function; public interface Option<T> { // オプションから値を取得する。 public T get(); // オプションから値を取得するが、値がない場合はdefaultValueを返す。 public T getOrElse(T defaultValue); // 値がない場合trueを戻す public boolean isEmpty(); // TからOption<S>を作るFunctionを受け取り、Option<S>を戻す。 ←NEW!! public <S> Option<S> bind(Function<T, Option<S>> func); }
実装
package sample.option; import com.google.common.base.Function; public abstract class AbstractOption<T> implements Option<T> { public <S> Option<S> bind(Function<T, Option<S>> function) { return isEmpty() ? None.<S>of() : function.apply(get()); } }
thisがNone<T>の時にNone<S>を、Some<T>の時は中身をget()で取り出してfunctionに渡した結果(Some<S>またはNone<S>)を戻している。
これらを使って、具体的に祖母取得メソッドであるgetGrandmum()の実装がどう変えられるのかを見て行く。
開始
SheepのgetFather()とgetMother()はfatherやmotherに値が設定されている場合にSomeを、設定されていない場合にNoneを戻す。値が無いかもしれないという仕様を明確にする為のもので、つまり戻り型がOption
public interface Sheep { /** @return この羊の父親がいれば父親を含んだOption(=Some)を、居なければ空のOption(=None)を戻す */ Option<Sheep> getFather(); /** @return この羊の母親がいれば母親を含んだOption(=Some)を、居なければ空のOption(=None)を戻す */ Option<Sheep> getMother(); }
実装
public class SheepImpl implements Sheep { private Sheep mother; private Sheep father; ... public Option<Sheep> getFather() { return father == null ? None.<Sheep>of() : Some.of(father); } public Option<Sheep> getMother() { return mother == null ? None.<Sheep>of() : Some.of(mother); } }
これを使って、祖母羊を取得するユーティリティメソッドを書こうとする。
public static Option<Sheep> getGrandmum(Sheep sheep) { return sheep == null ? None.<Sheep>of() : sheep.getMother().isEmpty() ? None.<Sheep>of() : sheep.getMother().get().getMother(); }
これをもうちょっと簡単に書けるようにしてみよう。
SheepのgetMother()を修正
getMother()の中身は、unit()を使って簡単に書ける。
import static sample.option.Options.unit; public class SheepImpl implements Sheep { ... public Option<Sheep> getMother() { // return mother == null ? None.<Sheep>of() : Some.of(mother); return unit(mother); } }
Functionの定義
メソッドのままだと合成がやりにくいので、getMother()をFunctionとして定義
public static Function<Sheep, Option<Sheep>> motherF = new Function<Sheep, Option<Sheep>>() { public Option<Sheep> apply(Sheep sheep) { return sheep.getMother(); } };
Function
getGrandmum()を書き直し
unitとbindを使って以下のように簡潔に書ける。
/** 祖母を返すかもしれないユーティリティメソッド*/ public static Option<Sheep> getGrandmum(Sheep sheep) throws Throwable { return unit(sheep).bind(motherF).bind(motherF); }
すっきりした!
曾々祖母も.bind(motherF)を後ろに増やすだけ。
/** 曾々祖母を返すかもしれないユーティリティメソッド */ public static Option<Sheep> getGreatGreatGrandmum(Sheep sheep) throws Throwable { return unit(sheep).bind(motherF).bind(motherF).bind(motherF).bind(motherF); }
テストデータとテストを用意。(データはこの辺の真似しました。)
// コンストラクタ new SheepImpl(名称, 父親, 母親)で生成 Sheep mary = new SheepImpl("mary", null, null); Sheep adam = new SheepImpl("adam", null, null); Sheep john = new SheepImpl("john", adam, null); Sheep mike = new SheepImpl("mike", null, mary); Sheep beth = new SheepImpl("beth", null, mary); Sheep suze = new SheepImpl("suze", john, beth); Sheep don = new SheepImpl("don", mike, beth); Sheep duke = new SheepImpl("duke", don, suze); Sheep[] sheeps = { mary, adam, john, mike, beth, suze, duke, null }; @Test public void testGrandmum() throws Throwable { assertEquals("suzueの祖母はmary", mary, getGrandmum(suze).get()); assertEquals("dukeの祖母はbeth", beth, getGrandmum(duke).get()); assertTrue("maryの祖母は居ない", getGrandmum(mary).isEmpty()); }
「ルールは大事よね」
モナドと言い張るからにはモナド則とかいうのを守らないといけないらしい。
以下の3つのルールについて、左右辺の関数が等しい必要がある。
関数が等しいということは直接テストできないけど、とりあえず同じ入力に対する出力が一致することを確認する。
その1.「(return x) >>= f ≡ f x」
@Test public void testRule1_mother() throws Throwable { for (Sheep sheep : sheeps) { assertEquals( unit(sheep).bind(motherF), motherF.apply(sheep) ); } } public static Function<Sheep, Option<Sheep>> fatherF = new Function<Sheep, Option<Sheep>>() { public Option<Sheep> apply(Sheep sheep) { return sheep.getFather(); } }; @Test public void testRule1_father() throws Throwable { for (Sheep sheep : sheeps) { assertEquals( unit(sheep).bind(fatherF), fatherF.apply(sheep) ); } }
その2. 「m >>= return ≡ m」
public static Function<Sheep, Option<Sheep>> unitF = new Function<Sheep, Option<Sheep>>() { public Option<Sheep> apply(Sheep sheep) { return unit(sheep); } }; @Test public void testRule2() throws Throwable { for (Sheep sheep : sheeps) { assertEquals( unit(sheep).bind(unitF), unit(sheep)); } }
その3. 「(m >>= f) >>= g ≡ m >>= ( \x -> (f x >>= g) )」
※ちょっと自信なし
Function<Sheep, Option<Sheep>> maternalGrandfather = new Function<Sheep, Option<Sheep>>() { public Option<Sheep> apply(Sheep obj) { return motherF.apply(obj).bind(fatherF); } }; @Test public void testRule3_maternalGrandfather() throws Throwable { for (Sheep sheep : sheeps) { assertEquals( unit(sheep).bind(motherF).bind(fatherF), unit(sheep).bind(maternalGrandfather)); } } Function<Sheep, Option<Sheep>> paternalGrandmother = new Function<Sheep, Option<Sheep>>() { public Option<Sheep> apply(Sheep obj) { return fatherF.apply(obj).bind(motherF); } }; @Test public void testRule3_paternalGrandmother() throws Throwable { for (Sheep sheep : sheeps) { assertEquals( unit(sheep).bind(fatherF).bind(motherF), unit(sheep).bind(paternalGrandmother)); } }
実際に動かすとnullの場合にtestRule1がこけるので、motherF、fatherFの方を直してやらないといけない。
public static Function<Sheep, Option<Sheep>> motherF = new Function<Sheep, Option<Sheep>>() { public Option<Sheep> apply(Sheep sheep) { if (sheep == null) { return unit(null); } return sheep.getMother(); } };
結局引数がnullの場合と、戻り値がnullの場合にNoneを返さないといけないので、if文は2カ所に必要なところがイマイチ。
まあ、しゃーなし。
追記: unit or returnの実装について
いろいろみてまわると、どこのMaybeモナドの実装でもreturn = Justにしてる。今回の例でいうと、
/** TからOption<T>を作る */ public static <T> Option<T> unit(T element) { return Some.of(element); }
で、Noneを作る場合はNone.of()を直接という方式。
今回自分が実装したように、unit()側でSomeを返すかNoneを返すかを判断するなら、Some/Noneというクラスを外部に露出させずに、外部からはunit()とOption型だけで書けるようになるので、そっちの方がいいような気がするんだけどどうなんだろう?
nullを対象に含めないとかそういう代数的な話のような気がするけど正直よくわからない。