なぜλ-calculusの最適なエバリュエーターは数式なしで大きなモジュラ指数を計算できるのですか?


135

教会番号は、関数としての自然数のエンコードです。

(\ f x  (f x))             -- church number 1
(\ f x  (f (f (f x))))     -- church number 3
(\ f x  (f (f (f (f x))))) -- church number 4

きちんと、2つの教会の数を適用するだけで指数化できます。つまり、4対2を適用すると、教会番号16、またはを取得します2^4。明らかに、それは全く実用的ではありません。教会の数は線形のメモリ量を必要とし、本当に非常に遅いです。10^10GHCIがすぐに正しく答えるようなものを計算すると、時間がかかり、とにかくコンピュータのメモリを合わせることができませんでした。

最近、最適なλエバリュエーターを使って実験しています。私のテストでは、最適なλ計算機に誤って次のように入力しました。

10 ^ 10 % 13

べき乗ではなく、乗法であると考えられていました。指を動かして、絶え間なく実行されているプログラムを絶望的に中止する前に、それは私の要求に答えました:

3
{ iterations: 11523, applications: 5748, used_memory: 27729 }

real    0m0.104s
user    0m0.086s
sys     0m0.019s

「バグアラート」が点滅しているので、Googleにアクセスして確認しました10^10%13 == 3しかし、λ計算機はその結果を見つけることを想定していなかったため、10 ^ 10をほとんど格納できません。私は科学についてそれを強調し始めました。それはすぐに私に答え20^20%13 == 350^50%13 == 460^60%3 == 0Haskell自体が(整数オーバーフローのために)計算できなかったため外部ツールを使用してこれらの結果を検証する必要がありました(もちろん、IntsではなくIntegersを使用している場合です!)。それを限界まで押し上げ、これが次の答えでした200^200%31

5
{ iterations: 10351327, applications: 5175644, used_memory: 23754870 }

real    0m4.025s
user    0m3.686s
sys 0m0.341s

宇宙の原子ごとに宇宙のコピーが1つあり、合計で原子ごとにコンピューターがあった場合、教会番号を保存できませんでした200^200。これにより、私のMacが本当にそれほど強力であるかどうかを尋ねるようになりました。おそらく、最適な評価者は、Haskellが遅延評価で行うのと同じ方法で、不要な分岐をスキップして答えにたどり着くことができたでしょう。これをテストするために、私はλプログラムをHaskellにコンパイルしました:

data Term = F !(Term -> Term) | N !Double
instance Show Term where {
    show (N x) = "(N "++(if fromIntegral (floor x) == x then show (floor x) else show x)++")";
    show (F _) = "(λ...)"}
infixl 0 #
(F f) # x = f x
churchNum = F(\(N n)->F(\f->F(\x->if n<=0 then x else (f#(churchNum#(N(n-1))#f#x)))))
expMod    = (F(\v0->(F(\v1->(F(\v2->((((((churchNum # v2) # (F(\v3->(F(\v4->(v3 # (F(\v5->((v4 # (F(\v6->(F(\v7->(v6 # ((v5 # v6) # v7))))))) # v5))))))))) # (F(\v3->(v3 # (F(\v4->(F(\v5->v5)))))))) # (F(\v3->((((churchNum # v1) # (churchNum # v0)) # ((((churchNum # v2) # (F(\v4->(F(\v5->(F(\v6->(v4 # (F(\v7->((v5 # v7) # v6))))))))))) # (F(\v4->v4))) # (F(\v4->(F(\v5->(v5 # v4))))))) # ((((churchNum # v2) # (F(\v4->(F(\v5->v4))))) # (F(\v4->v4))) # (F(\v4->v4))))))) # (F(\v3->(((F(\(N x)->F(\(N y)->N(x+y)))) # v3) # (N 1))))) # (N 0))))))))
main = print $ (expMod # N 5 # N 5 # N 4)

これは正しく15 ^ 5 % 4)を出力しますが、上に何かを投げる10^10とスタックし、仮説を排除します。

私が使用し、最適な評価は、指数関数的モジュラス数学の任意の並べ替えが含まれていませんでした長い160ライン、最適化されていないJavaScriptプログラムである-と私は同じように簡単だった使用ラムダ計算モジュラス機能:

ab.(bcd.(ce.(dfg.(f(efg)))e))))(λc.(cde.e)))(λc.(a(bdef.(dg.(egf))))(λd.d)(λde.(ed)))(bde.d)(λd.d)(λd.d))))))

特定のモジュラー算術アルゴリズムや公式は使用していません。では、最適な評価者はどのようにして正しい答えを得ることができるのでしょうか?


2
使用する最適な評価の種類について詳しく教えてください。おそらく紙の引用?ありがとう!
Jason Dagit、2015

11
関数型プログラミング言語の最適な実装』の本説明されているように、私はランピングの抽象的なアルゴリズムを使用しています。「オラクル」(クロワッサン/ブラケットなし)を使用していないことに注意してください。その用語はEALタイプ可能なためです。また、代わりにランダムに並列にファンを減少させる、...到達不能なノードを低減しないよう私が順次グラフを横断していたが、私は、これは文献私の知る限りではない怖い
MaiaVictor

7
さて、誰かが気になった場合のために、私は最適なエバリュエーター用のソースコードでGitHubリポジトリを設定しました。多くのコメントがあり、実行してテストできますnode test.js。ご不明な点がありましたらお知らせください。
MaiaVictor 2015

1
きちんと見つける!最適な評価についてはよくわかりませんが、フェルマーのリトル定理/オイラーの定理を思い出させてくれると言えます。あなたがそれを知らないなら、それは良い出発点かもしれません。
ルキ、2015

5
これは、質問の内容について少しでも手掛かりがわからないのは初めてですが、それでも質問、特に優れた最初の事後の回答に賛成票を投じます。
Marco13、2015

回答:


124

現象は、Haskellスタイルの遅延評価(またはこの点ではそれほど遠くない通常の値による呼び出し)とVuillemin-Lévy-Lamping-で劇的に異なる可能性がある共有ベータ削減ステップの量から生じます。 Kathail-Asperti-Guerrini-(et al…)「最適な」評価。これは一般的な機能であり、この特定の例で使用できる算術式から完全に独立しています。

共有とは、1つの「ノード」が実際のラムダタームのいくつかの同様の部分を表すことができる、ラムダタームの表現を持つことを意味します。たとえば、用語を表すことができます

\x. x ((\y.y)a) ((\y.y)a)

を表すサブグラフが1つだけ出現する(有向非循環)グラフ(\y.y)aと、そのサブグラフをターゲットとする2つのエッジを使用する。Haskellの用語では、1回だけ評価する1つのサンクと、このサンクへの2つのポインターがあります。

Haskellスタイルのメモ化は、完全なサブタームの共有を実装します。この共有レベルは、有向非循環グラフで表すことができます。最適な共有にはこの制限はありません。「部分的な」サブタームを共有することもできるため、グラフ表現に循環が含まれる可能性があります。

これら2つの共有レベルの違いを確認するには、用語

\x. (\z.z) ((\z.z) x)

Haskellの場合のように、共有が完全なサブタームに制限されている場合、の発生は1つだけかもしれません\z.zが、ここでの2つのベータredexesは異なります:1つはでもう(\z.z) x1つはです(\z.z) ((\z.z) x)。共有することはできません。部分的なサブタームの共有が許可されいる場合、この引数が何であれ、1つのステップで評価される部分的なターム(\z.z) [](関数だけ\z.zでなく、「関数に\z.z適用される関数」)を共有することが可能になります。 1つのノードのみが2つのアプリケーションを表すグラフを作成できます。\z.zこれらの2つのアプリケーションは、たった1つのステップで削減できます。「最初の発生」の引数は正確に「2番目の発生」であるため、このノードにはサイクルがあることに注意してください。最後に、最適な共有を使用すると、ベータ削減(およびいくつかの簿記)の1つのステップで\x. (\z.z) ((\z.z) x))結果を(表すグラフ)から(表すグラフ)に移動できます\x.x。これは基本的に、最適なエバリュエーターで発生することです(そしてグラフ表現は、空間の爆発を防ぐものでもあります)。

少し拡張された説明については、論文「弱い最適性」と「共有の意味」をご覧ください(あなたが興味を持っているのは、序論とセクション4.1、そしておそらく最後にある参考文献のポインタです)。

あなたの例に戻って、チャーチ整数に作用する算術関数のコーディングは、最適な評価者が主流の言語よりも優れたパフォーマンスを発揮できる「よく知られた」例の1つです(この文では、実際によく知られていることは、専門家はこれらの例を認識しています)。このような例については、AspertiとChroboczekによる「Safe Operators:Brackets Closed Forever」を参照してください(ちなみに、ここではEALで型付けできない興味深いラムダ用語が見つかるので、このアスペルティ/クロボチェク紙から始まるオラクルを見てください)。

ご存知のように、この種のエンコーディングはまったく実用的ではありませんが、何が起こっているのかを理解するための優れた方法です。そして、さらに調査するための課題で締めくくりましょう。これらのおそらく悪いエンコーディングに対する最適な評価が、妥当なデータ表現に対する従来の評価と実際に同等である例を見つけることができますか?(私が知る限り、これは本当に未解決の質問です)。


34
これは、非常に珍しく完全な最初の投稿です。StackOverflowへようこそ!
dfeuer

2
洞察力に他ならない。ありがとう、そしてコミュニティへようこそ!
MaiaVictor 2015

7

これはアンサーではありませんが、どこから探し始めるのかを示唆しています。

特に書き換えによって、小さな空間でモジュラー指数を計算する簡単な方法があります

(a * x ^ y) % z

なので

(((a * x) % z) * x ^ (y - 1)) % z

エバリュエーターがこのように評価し、累積パラメーターaを通常の形式で保持する場合は、スペースを使いすぎないようにします。実際にあなたのエバリュエーター最適である場合、おそらくそれこのエバリュエーターよりも多くの作業をしてはならないので、特にこのエバリュエーター評価するのにかかる時間より多くのスペースを使用することはできません。

最適なエバリュエーターが本当に何であるか本当にわからないので、これをもっと厳密にすることはできません。


4
@トムが言うように、@ Viclibフィボナッチは良い例です。fib素朴な方法で指数時間を必要とします。これは単純なメモ化/動的プログラミングで線形に短縮できます。の対数(!)時間でさえ、[[0,1],[1,1]](各乗算を一定のコストとするためにカウントする限り)のn乗の行列を計算することで可能です。
2015

1
大まかに近づくほど大胆であれば、一定の時間でさえ:)
J.アブラハムソン

5
@TomEllisしかし、任意のラムダ計算式を削減する方法だけを知っているものに、なぜそのような考えが(a * b) % n = ((a % n) * b) % nあるのでしょうか?それは間違いなく神秘的な部分です。
リードバートン

2
@ReidBarton確かに試してみました!ただし、同じ結果です。
MaiaVictor 2015

2
@TomEllisとChiですが、ちょっとした発言があります。従来の再帰関数は「ナイーブ」なfib実装であると想定していますが、IMOには、より自然な方法でそれを表現する別の方法があります。その新しい表現の通常の形式は、従来の表現の半分のサイズです)、Optlamは、線形表現を線形に計算します!したがって、λ計算に関する限り、これはfibの「単純な」定義であると主張します。私はブログ投稿を作成しますが、それが本当に価値があるかどうかは
わかり
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.