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(sel)じゃ駄目なのか?


やっぱりよく分からない。

使用例

「環境」が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
...