数値のFizz、Buzz等への変換を、Javaで通常より直感的に表現したい

お題: 数値のFizz、Buzz等への変換を、Smalltalk(とRuby)で通常より直感的に表現したい - Smalltalkのtは小文字です

ここで #fizz には n (#fizz の文脈ではレシーバーである self)が 3 で割り切れるときに、'Fizz' を返す代わりに「キー -> 値」で生成できる Association のインスタンスを返させています。もし 3 で割り切れなければ n (つまり self)をそのまま返させます。なお、Smalltalk では返値を指定しなければ self が自動的に返るので最後の ^self という記述は別になくても構いません。
結果、続く buzz というメッセージは、メソッド #fizz をスルー(^self)してきた n それ自身か、n->'Fizz' という Association のインスタンスのいずれかに送られることになります。当然、その結果としてコールされる #buzz についても通常どおりの Integer のみならず Association のほうにも定義しておく手間が増えてしまうわけですが―

Integer >> buzz
(self isDivisibleBy: 5) ifTrue: [^self->'Buzz'].
^self

Association >> buzz
(key isDivisibleBy: 5) ifTrue: [^key->(value, 'Buzz')].
^self

型の動きを整理すると、

  • 数値にbuzzを送るとAssociationになる
  • Associationにbuzzを送ると、valueを取り出して'Buzz'を加えたAssociationになる

これってなんか昨日のアレに似てるよね。
ということでJavaを使ってその線で実装してみた。

実装

Javaの場合は数値にメソッドを追加とか器用なことは出来ないので、数値の場合もAsscociationに変換することにする。
一応名前はAsscociationにしているけど、勝手に作ったインタフェースなので汎用的な意味はないです。
あとFunctionとあとで出てくるCollections2の為にguava-libraries使ってます。

package sample.fizzbuzz;

import com.google.common.base.Function;

public interface Association {
    /** @return 最終的に表示する値を取り出す */
    Object value();
    /** @return 関連づけられた数値を取り出す */
    Integer getNumber();
    /** @return prefixにメッセージ文言を追加して戻す */
    String makeMessage(String prefix);

    /** thisとfunctionから自然な感じでAssociationを戻す */
    Association bind(Function<Integer, Association> function);
}

1〜100の数値を「Number」、"Fizz"や"Buzz"などの文言を「Message」と呼んでいる。
makeMessage(String)は"Fizz"に"Buzz"をくっつけたりするのに使っている。


数値の保持はどの場合も必要なので抽象スーパークラスに書く。
あとequals()とhashCode()も定義しておく。*1

package sample.fizzbuzz;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

public abstract class AbstractAssociation implements Association {
    protected final Integer number;
    public AbstractAssociation(Integer number) {
        this.number = number;
    }
    public final Integer getNumber() {
        return number;
    }
    public final boolean equals(Object other) {
        return EqualsBuilder.reflectionEquals(this, other);
    }
    public final int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }
}


数値に対するAssociationを定義する。

package sample.fizzbuzz;

import com.google.common.base.Function;

public class NumberAssociation extends AbstractAssociation {
    private NumberAssociation(Integer number) {
        super(number);
    }
    // ファクトリメソッド
    public static Association valueOf(Integer number) {
        return new NumberAssociation(number);
    }
    // 数値を値とする
    public Object value() {
        return number;
    }
    // 文言を作成する。数値は付与せずprefixをそのまま戻す
    public String makeMessage(String prefix) {
        return prefix;
    }
    // function.apply()の戻り値をそのまま戻す
    public Association bind(Function<Integer, Association> function) {
        return function.apply(number);
    }
}


文言に対するAssociationを定義する。

package sample.fizzbuzz;

import com.google.common.base.Function;

public class MessageAssociation extends AbstractAssociation {
    /** "Fizz"などの表示文言を保持する */
    protected final String message;
    private MessageAssociation(Integer number, String message) {
        super(number);
        this.message = message;
    }
    // ファクトリメソッド
    public static MessageAssociation valueOf(Integer number, String message) {
        return new MessageAssociation(number, message);
    }
    // 保持する文言を値とする
    public Object value() {
        return message;
    }
    // 文言を作成する。prefixに自分自身の文言を付け足して戻す。
    public String makeMessage(String prefix) {
        return prefix + message;
    }
    // function.apply()の戻り値と自分の文言から新たなMessageAssociationを生成して戻す
    public Association bind(Function<Integer, Association> function) {
        return new MessageAssociation(number, function.apply(number).makeMessage(message));
    }
}


数値からAssociationを作るunit(いわゆるreturn)を定義する。

package sample.fizzbuzz;

public class Associations {
    public static Association unit(Integer number) {
        return NumberAssociation.valueOf(number);
    }
}

では実際にFizzBuzzを書いてみる。
fizzおよびbuzzを、数値を取ってAssociationを戻す関数とし定義する。
ベタに書くとこうなる。

    final Function<Integer, Association> fizz = new Function<Integer, Association>() {
        public Association apply(Integer number) {
            return number != null && number % 3 == 0
                ? MessageAssociation.valueOf(number, "Fizz")
                : NumberAssociation.valueOf(number);
        }
    };

二個作るので、実クラス化しておく。

package sample.fizzbuzz;

import com.google.common.base.Function;

// dividerで割り切れる時にmessageを返す
// それ以外とnullの時はNumberAssociationを戻す
public class FizzBuzzOperator implements Function<Integer, Association> {
    private int divider;
    private String message;
    public FizzBuzzOperator(int divider, String message) {
        this.divider = divider;
        this.message = message;
    }
    public Association apply(Integer number) {
        return number != null && number % divider == 0
            ? MessageAssociation.valueOf(number, message)
            : NumberAssociation.valueOf(number);
    }
}

あらためて、fizzとbuzzを定義

    final Function<Integer, Association> fizz = new FizzBuzzOperator(3, "Fizz");
    final Function<Integer, Association> buzz = new FizzBuzzOperator(5, "Buzz");

1〜100を作るところと表示するところ。

    public void test() {
        // 1〜100のListを作成
        List<Integer> sequence = new ArrayList<Integer>();
        for (int i = 1; i <= 100; i++) {
            sequence.add(Integer.valueOf(i));
        }
        // fizzbuzz()呼び出し
        Collection<Object> results = fizzbuzz(sequence);
        // 結果を表示
        for (Object value : results) {
            System.out.printf("%s\n", value);
        }
    }

本体。Collections2.transform()はいわゆるmapですわ。

    public Collection<Object> fizzbuzz(List<Integer> sequence) {
        return Collections2.transform(sequence, new Function<Integer, Object>() {
            public Object apply(Integer n) {

                return unit(n).bind(fizz).bind(buzz).value();

            }
        });
    }

「n fizz buzz value」に近い感じで表現できていないだろうか。


pizz quzz razzはこう

    public Collection<Object> pizzquzzrazz(List<Integer> sequence) {
        final Function<Integer, Association> pizz = new FizzBuzzOperator(3, "Pizz");
        final Function<Integer, Association> quzz = new FizzBuzzOperator(5, "Quzz");
        final Function<Integer, Association> razz = new FizzBuzzOperator(7, "Razz");

        return Collections2.transform(sequence, new Function<Integer, Object>() {
            public Object apply(Integer n) {

                return unit(n).bind(pizz).bind(quzz).bind(razz).value();

            }
        });
    }

「ルールは(略

モナド則を満たしているか。
等しいということは直接テストできないけど、とりあえず同じ入力に対する出力が一致することを確認する。

    final Function<Integer, Association> fizz = new FizzBuzzOperator(3, "Fizz");
    final Function<Integer, Association> buzz = new FizzBuzzOperator(5, "Buzz");
    final Function<Integer, Association> unit = new Function<Integer, Association>() {
        public Association apply(Integer number) {
            return unit(number);
        }
    };
    final List<Integer> sequence = Collections.unmodifiableList(new ArrayList<Integer>() {{
        for (int i = 1; i <= 100; i++) {
            add(Integer.valueOf(i));
        }
    }});

    // その1.「(return x) >>= f ≡ f x」
    @Test
    public void testRule1() throws Throwable {
        for (Integer value : sequence) {
            assertEquals(
                    unit(value).bind(fizz),
                    fizz.apply(value));
        }
    }
    @Test
    public void testRule1Null() throws Throwable {
        assertEquals(
                unit(null).bind(fizz),
                fizz.apply(null));
    }

    // その2. 「m >>= return ≡ m」
    @Test
    public void testRule2_number() throws Throwable {
        for (Integer value : sequence) {
            assertEquals(
                    unit(value).bind(unit),
                    unit(value));
        }
    }
    @Test
    public void testRule2_message() throws Throwable {
        for (Integer value : sequence) {
            assertEquals(
                    unit(value).bind(fizz).bind(unit),
                    unit(value).bind(fizz));
        }
    }


    // その3. 「(m >>= f) >>= g ≡ m >>= ( \x -> (f x >>= g) )」
    @Test
    public void testRule3() throws Throwable {
        Function<Integer, Association> fizzbuzz =
            new Function<Integer, Association>() {
                public Association apply(Integer obj) {
                    return fizz.apply(obj).bind(buzz);
                }
            };
        for (Integer value : sequence) {
            assertEquals(
                    unit(value).bind(fizz).bind(buzz),
                    unit(value).bind(fizzbuzz));
        }
    }

*1:手抜きでCommons Langのリフレクション系のメソッド使ってます。