JavaでReaderモナドでFizzBuzz
例のごとく「All About Monads」を読んでたら、Readerモナドでテンプレートというサンプルが載っていたので、テンプレートエンジン厨としてはこれは是非会得しておきたい。
がしかし、内容がイマイチよく分からないので、とりあえず練習で写経してみた。
Readerモナド
Readerモナドは「変数束縛あるいは他の共有環境の保持」に利用するとある。
内容を見ると、環境から値を読み出す「読み取り機」を包んだモナドで、それに環境を与えることで値を得ることができるようだ。
Haskellでの実装はこんな感じらしい:
newtype Reader e a = Reader { runReader :: (e -> a) } instance Monad (Reader e) where return a = Reader $ \e -> a (Reader r) >>= f = Reader $ \e -> runReader (f (r e)) e
Javaに置き換えてみる。returnはunit()という名前、>>=はbind()という名前で実装するよ。
Functionはguavaの実装使うけど自分で書いてもいいよ。
package sample.reader; import com.google.common.base.Function; public class Reader<E, T> { private final Function<E, T> runReader; private Reader(Function<E, T> runReader) { if (runReader == null) { throw new IllegalArgumentException("runReader should not be null."); } this.runReader = runReader; } public T runReader(E e) { return runReader.apply(e); } // return a = Reader $ \e -> a public static <E, T> Reader<E, T> unit(final T value) { return new Reader<E, T>(new Function<E, T>() { @Override public T apply(E dummy) { return value; } }); } // (Reader r) >>= f = Reader $ \e -> runReader (f (r e)) e public <S> Reader<E, S> bind(final Function<T, Reader<E, S>> f) { return new Reader<E, S>(new Function<E, S>() { @Override public S apply(E e) { return f.apply(Reader.this.runReader(e)).runReader(e); } }); } // ... }
Reader $ \e -> runReader (f (r e)) e
はくどく書くとReader(\e -> (runReader (f (r(e))))(e) )
で、「左値Readerの中身rを引数eで呼び出した結果をfに渡し、得られたReaderの中身をrunReaderで取り出して引数eで呼び出し」になる。あってるはず。
他にも便利関数が定義されてるのであわせて移植する。
// 上の// ...の部分に追加 // ask = Reader id public static <E> Reader<E, E> ask() { Function<E, E> id = new Function<E, E>() { @Override public E apply(E e) { return e; } }; return new Reader<E, E>(id); } // local f c = Reader $ \e -> runReader c (f e) public <D> Reader<D, T> local(final Function<D, E> f) { return new Reader<D, T>(new Function<D, T>() { @Override public T apply(D d) { return runReader(f.apply(d)); } }); } // asks sel = ask >>= return . sel public static <E, T> Reader<E, T> asks(final Function<E, T> sel) { return Reader.<E>ask().bind(new Function<E, Reader<E, T>>() { @Override public Reader<E, T> apply(E e) { return unit(sel.apply(e)); } }); }
askは恒等関数を包んだReaderで、渡した環境をそのまま戻すようなReaderになる。
localは環境の型を変えるような処理をおこなう。アダプター的な関数?Readerを第二引数で渡すのではなくインスタンスメソッドで実装しthisを使うようにした。
asksはaskにreturnとselを合成した関数をバインドする、ということみたいだけど、new Reader
じゃ駄目なのか?
やっぱりよく分からない。
使用例
「環境」がJavaのシステムプロパティの場合で考えてみた。
ホームディレクトリを読み取るReaderの実装はこんな感じかな。
private static final String HOME_KEY = "user.home"; private final Reader<Properties, String> homeReader = Reader.asks(new Function<Properties, String>() { @Override public String apply(Properties properties) { return properties.getProperty(HOME_KEY); } });
同じくファイルセパレータを読み取るReader
private static final String FILE_SEPARATOR_KEY = "file.separator"; private final Reader<Properties, String> fileSeparatorReader = Reader.asks(new Function<Properties, String>() { @Override public String apply(Properties properties) { return properties.getProperty(FILE_SEPARATOR_KEY); } });
ホームディレクトリの直下に"workspace"というディレクトリがあると仮定して、そのパスを導出する。
private static final String WORKSPACE_NAME = "workspace"; private final Reader<Properties, String> workspacePathReader = fileSeparatorReader.bind(new Function<String, Reader<Properties, String>>() { @Override public Reader<Properties, String> apply(final String fileSeparator) { return homeReader.bind(new Function<String, Reader<Properties, String>>() { @Override public Reader<Properties, String> apply(String homePath) { return Reader.unit(homePath + fileSeparator + WORKSPACE_NAME); } }); } });
何がうれしいのか良く分からないけど……
動かしてみる。
public void setup(Properties properties) { String workspacePath = workspacePathReader.runReader(properties); System.err.println("workspace path: " + workspacePath); ... }
Propertiesじゃなくて設定ファイルか何かから読み込んだMapだったら?
public void setup(Map<String, String> map) { Function<Map<String, String>, Properties> map2properties = new Function<Map<String, String>, Properties>() { @Override public Properties apply(Map<String, String> map) { Properties properties = new Properties(); properties.putAll(map); return properties; } }; Reader<Map<String, String>, String> workspacePathReaderForMap = workspacePathReader.local(map2properties); String workspacePath = workspacePathReaderForMap.runReader(map); System.err.println("workspace path: " + workspacePath); ... }
ありがたみが分からないのは、使い方が間違ってるからなのかな……
ReaderモナドでFizzBuzz
「http://d.hatena.ne.jp/mclh46/20110811/1313029938」にHaskellのReaderモナドでFizzBuzzを実装した例があるので写経してみる。
mapとかfmapとか用意してないのでbindのみでごりごり書くよ。
public Reader<Integer, String> makeFizzBuzz() { // fizz' = asks fizz final Reader<Integer, String> fizz = Reader.asks( new Function<Integer, String>() { @Override public String apply(Integer n) { return (n % 3 == 0) ? "Fizz" : ""; } }); // buzz' = asks buzz final Reader<Integer, String> buzz = Reader.asks( new Function<Integer, String>() { @Override public String apply(Integer n) { return (n % 5 == 0) ? "Buzz" : ""; } }); // str' = (++)<$>fizz'<*>buzz' Reader<Integer, String> strReader = fizz.bind(new Function<String, Reader<Integer,String>>() { @Override public Reader<Integer, String> apply(final String fstr) { return buzz.bind(new Function<String, Reader<Integer, String>>(){ @Override public Reader<Integer, String> apply(String bstr) { return Reader.unit(fstr + bstr); } }); } }); // number = show<$>ask final Reader<Integer, String> numberReader = Reader.asks(new Function<Integer, String>() { @Override public String apply(Integer n) { return n.toString(); } }); // fizzbuzz = do // str <- str' // if str=="" then number else return str Reader<Integer, String> fizzBuzz = strReader.bind(new Function<String, Reader<Integer,String>>() { @Override public Reader<Integer, String> apply(String str) { return str.equals("") ? numberReader : Reader.<Integer, String>unit(str); } }); return fizzBuzz; }
numberはちょっとはしょりました。
実行してみる
Reader<Integer, String> fizzBuzz = makeFizzBuzz(); for (int i = 1; i <= 100; i++) { System.out.println(fizzBuzz.runReader(i)); }
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 ...