Javaでチャーチ数
「関数型のテンプレートエンジンで計算 - terazzoの日記」でテンプレートエンジン上でチャーチ数を使って計算してみる際に、どうにも内容が理解できずに詰まったので、Javaで実装してみた。
プログラムとしての動きをつかむことが目的なので全部をλで書き下したりはしていない。
下のサイトがとても参考になった。しかし型があるって素晴らしい。
チャーチ数を表すインタフェース
まず、チャーチ数の定義だけど、
直感的には、数 n はラムダ式では f という関数をもらってそれを n 回適用したものを返す関数である。つまり、チャーチ数は1引数関数を受け取り、1引数関数を返す高階関数である。
ラムダ計算 - Wikipedia
とあるけど関数を戻すのは面倒なので関数fと中身xを引数にとり、xにfをn回適用した結果を戻す形で定義する。
public interface Church { <T> T apply(Function<T, T> f, T x); }
Functionは例によってGoogle guavaの奴です。自分で定義してもいい。
最初インタフェースに型パラメータを付けていたけど、それだとべき乗やデクリメントで困るので、型パラメータはメソッドに付けている。
zeroおよび数値の直接実装例
インタフェース Churchを使って、まず0を実装する。
fとxを取り、xにfを0回適用したって事は、つまり適用せずにxそのままが値になる。
Church zero = new Church() { public <T> T apply(Function<T, T> f, T x) { return x; } };
次に、1と2を直接実装してみる。
Church one = new Church() { public <T> T apply(Function<T, T> f, T x) { return f.apply(x); } }; Church two = new Church() { public <T> T apply(Function<T, T> f, T x) { return f.apply(f.apply(x)); } };
ちゃんと定義できたかを確認する。
fに「渡された値に1を足す」という処理を指定し、xに「0」を指定すれば、ChurchをIntegerに変換できる。
// Church nをIntegerに変換する。 public static Integer toInteger(Church n) { Function<Integer, Integer> inc = new Function<Integer, Integer>() { public Integer apply(Integer n) { return n + 1; } }; return n.apply(inc, 0); }
zero, one, twoを表示してみる。
@Test public void testNums() { Integer zeroNum = toInteger(zero); Integer oneNum = toInteger(one); Integer twoNum = toInteger(two); assertEquals(0, zeroNum.intValue()); assertEquals(1, oneNum.intValue()); assertEquals(2, twoNum.intValue()); System.out.printf("zero = %d\n", zeroNum); System.out.printf("one = %d\n", oneNum); System.out.printf("two = %d\n", twoNum); }
結果
zero = 0 one = 1 two = 2
succの実装
数をインクリメントするメソッドsuccを実装する。
n回fを適用したものに、もう一回fを適用すればよい。
Church succ(final Church n) { return new Church() { public <T> T apply(Function<T, T> f, T x) { return f.apply(n.apply(f, x)); } }; }
適用してみる。
Church three = succ(two); Church four = succ(three); Church five = succ(four);
中身を表示して確認してみる
@Test public void testSucc() { Integer threeNum = toInteger(three); Integer fourNum = toInteger(four); Integer fiveNum = toInteger(five); assertEquals(3, threeNum.intValue()); assertEquals(4, fourNum.intValue()); assertEquals(5, fiveNum.intValue()); System.out.printf("three = %d\n", threeNum); System.out.printf("four = %d\n", fourNum); System.out.printf("five = %d\n", fiveNum); }
表示
three = 3 four = 4 five = 5
plusの実装
数同士を足し合わせるplusを定義してみる。
「fをn回適用したものに、fをm回適用する」機能を持つChurchを戻す。
Church plus(final Church m, final Church n) { return new Church() { public <T> T apply(Function<T, T> f, T x) { return m.apply(f, n.apply(f, x)); } }; }
足してみる。
Church six = plus(two, four); Church seven = plus(four, three); Church eight = plus(three, five);
中身を表示して確認してみる
@Test public void testPlus() { Integer sixNum = toInteger(six); Integer sevenNum = toInteger(seven); Integer eightNum = toInteger(eight); assertEquals(6, sixNum.intValue()); assertEquals(7, sevenNum.intValue()); assertEquals(8, eightNum.intValue()); System.out.printf("six = %d\n", sixNum); System.out.printf("seven = %d\n", sevenNum); System.out.printf("eight = %d\n", eightNum); }
表示
six = 6 seven = 7 eight = 8
multiplyの実装
数同士を足し合わせるmultiplyを定義してみる。
「fをn回適用する」という処理をxにm回適用する。一応変数名をyに変えているが特に必要はない
Church multiply(final Church m, final Church n) { return new Church() { public <T> T apply(final Function<T, T> f, T x) { return m.apply(new Function<T, T>() { public T apply(T y) { return n.apply(f, y); } }, x); } }; }
結果を確認
@Test public void testMultiply() { Integer oneByZeroNum = toInteger(multiply(one, zero)); Integer threeByTwoNum = toInteger(multiply(three, two)); assertEquals(6, threeByTwoNum.intValue()); assertEquals(0, oneByZeroNum.intValue()); System.out.printf("three x two = %d\n", threeByTwoNum); System.out.printf("one x zero = %d\n", oneByZeroNum); }
three x two = 6 one x zero = 0
powerの実装
べき乗の計算を実装する。
ここから急に難しくなる。fにnをm回適用し、その結果をxに適用するらしい。
戻り値や引数の型から、Function型の型パラメータを推測しながら当てはめていく。
// (lambda (f) (lambda (x) (((m n) f) x))) ) Church power(final Church n, final Church m) { return new Church() { public <T> T apply(Function<T, T> f, T x) { return m.<Function<T, T>>apply( new Function<Function<T, T>, Function<T, T>>(){ public Function<T, T> apply(final Function<T, T> _f) { return new Function<T, T>() { public T apply(T _x) { return n.apply(_f, _x); } }; } }, f ).apply(x); } }; }
内側の_fと_xはどの値か明確にしたかっただけで、特に名前を変える必要はないです。
動かしてみる。
@Test public void testPower() { Integer fourPowerOfThreeNum = toInteger(power(four, three)); Integer fivePowerOfZeroNum = toInteger(power(five, zero)); assertEquals(64, fourPowerOfThreeNum.intValue()); assertEquals(1, fivePowerOfZeroNum.intValue()); System.out.printf("four ^ three = %d\n", fourPowerOfThreeNum); System.out.printf("five ^ zero = %d\n", fivePowerOfZeroNum); }
出力
four ^ three = 64 five ^ zero = 1
predの実装
succの逆、つまり一つ小さい数(predecessor)を戻す処理を考える。
いや、考えても分からないので戻り値や引数の型から、型を推測しながら写経していく。
Church pred(final Church n) { return new Church() { public <T> T apply(final Function<T, T> f, final T x) { return (n.<Function<Function<T, T>,T>>apply( new Function<Function<Function<T, T>, T>, Function<Function<T, T>, T>>() { public Function<Function<T, T>, T> apply(final Function<Function<T, T>, T> g) { return new Function<Function<T, T>, T>(){ public T apply(Function<T, T> h) { return h.apply(g.apply(f)); } }; } }, new Function<Function<T, T>,T>() { public T apply(Function<T, T> f) { return x; } } )) .apply(new Function<T, T>() { public T apply(T u) { return u; }}); } }; }
動かしてみる。
@Test public void testPred() { Integer predOfThreeNum = toInteger(pred(three)); Integer predOfZero = toInteger(pred(zero)); assertEquals(2, predOfThreeNum.intValue()); assertEquals(0, predOfZero.intValue()); System.out.printf("pred(three) = %d\n", predOfThreeNum); System.out.printf("pred(zero) = %d\n", predOfZero); Integer predOfSuccOfSixNum = toInteger(pred(succ(six))); assertEquals(toInteger(six), predOfSuccOfSixNum); }
出力。
pred(three) = 2 pred(zero) = 0
負の数はなくpred(zero)はzeroになる。
minusの実装
引き算を実装する。
mからnを引くには、mにn回predを適用すればよい。
// n pred m Church minus(final Church m, final Church n) { return n.apply(new Function<Church, Church>() { public Church apply(Church c) { return pred(c); } }, m); }
実行してみる。
@Test public void testMinus() { Integer fiveFromEightNum = toInteger(minus(eight, five)); Integer threeFromTwoNum = toInteger(minus(two, three)); assertEquals(3, fiveFromEightNum.intValue()); assertEquals(0, threeFromTwoNum.intValue()); System.out.printf("eight - five = %d\n", fiveFromEightNum); System.out.printf("two - three = %d\n", threeFromTwoNum); }
出力
eight - five = 3 two - three = 0
負の数はなくmよりnが大きい場合はzeroになる。
割り算などの他の計算
ラムダ計算の範囲でいけるらしい。
今回は主旨から外れるので深入りしない。