Чому оптимальні оцінювачі λ-числення здатні обчислювати великі модульні експоненції без формул?


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^10- на яке GHCI швидко відповідає правильно - зайняло б віки і ніяк не могло вмістити пам'ять на вашому комп’ютері.

Останнім часом я експериментував з оптимальними λ оцінювачами. На своїх тестах я випадково набрав на своєму оптимальному λ-калькуляторі таке:

10 ^ 10 % 13

Це повинно було бути примноження, а не експоненція. Перш ніж я зміг відчайдушно перевести пальці, щоб перервати вічну програму, він відповів на моє прохання:

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

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

Коли миготіло повідомлення про помилку, я 10^10%13 == 3дійсно перейшов до Google і перевірив . Але λ-калькулятор не повинен був знайти цього результату, він ледве може зберігати 10 ^ 10. Я почав це підкреслювати, для науки. Він негайно відповів мені 20^20%13 == 3, 50^50%13 == 4, 60^60%3 == 0. Мені довелося використовувати зовнішні інструменти для перевірки цих результатів, оскільки сам Haskell не зміг їх обчислити (через ціле переповнення) (це, звичайно, якщо ви використовуєте Integers не Ints!). Штовхаючи його до своїх меж, це була відповідь на 200^200%31:

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

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

Якби у нас була одна копія Всесвіту для кожного атома у Всесвіті, і у нас був комп'ютер для кожного атома, який ми мали загалом, ми не могли б зберігати номер церкви 200^200. Це змусило мене запитати, чи справді мій мак такий сильний. Можливо, оптимальний оцінювач зміг пропустити непотрібні гілки і прийти прямо до відповіді так само, як і 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)

Це правильно виводить 1( 5 ^ 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
Чи можете ви розповісти більше про тип оптимальної оцінки, який ви використовуєте? Можливо, цитата на папері? Дякую!
Джейсон Дагіт

11
Я використовую абстрактний алгоритм Лампінга, як це пояснено в книзі «Оптимальне втілення мов функціонального програмування» . Зауважте, що я не використовую "оракул" (без круасанів / дужок), оскільки цей термін може застосовуватися EAL. Крім того, замість випадкового скорочення вентиляторів паралельно, я послідовно об'їжджаю графік, щоб не зменшити недоступні вузли, але боюся, це не в літературі AFAIK ...
MaiaVictor

7
Добре, якщо комусь цікаво, я створив сховище GitHub із вихідним кодом для мого оптимального оцінювача. У ньому є багато коментарів, і ви можете протестувати його на роботі node test.js. Повідомте мене, якщо у вас є питання.
MaiaVictor

1
Охайна знахідка! Я недостатньо знаю про оптимальне оцінювання, але можу сказати, що це нагадує мені маленьку теорему Ферма / теорему Ейлера. Якщо ви не знаєте про це, це може бути хорошою відправною точкою.
luqui

5
Це перший раз, коли я не маю ні найменшого поняття, про що йдеться, але все-таки підтверджую це питання, зокрема, видатний перший відповідь після повідомлення.
Marco13

Відповіді:


124

Це явище походить від кількості спільних кроків бета-зменшення, які можуть бути кардинально різними в ледачій оцінці в стилі Хаскелла (або звичайному дзвінку за значенням, що в цьому відношенні не так далеко) і в Вуйлемін-Леві-Лампінг- Kathail-Asperti-Guerrini- (та ін.…) "Оптимальна" оцінка. Це загальна особливість, яка абсолютно не залежить від арифметичних формул, які ви могли використовувати в цьому конкретному прикладі.

Спільний доступ означає представлення вашого лямбда-терміна, в якому один "вузол" може описати кілька подібних частин фактичного лямбда-терміна, який ви представляєте. Наприклад, ви можете представляти цей термін

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

використовуючи (спрямований ациклічний) графік, у якому є лише одне виникнення підграфа, що представляє (\y.y)a, та два ребра, орієнтовані на цей підграф. З точки зору Haskell, у вас є один громовідвід, який ви оцінюєте лише один раз, і два покажчики на цю груди.

Пам’ятка у стилі Haskell реалізує обмін повними підтермінами. Цей рівень спільного використання може бути представлений спрямованими ациклічними графіками. Оптимальне спільне використання не має цього обмеження: воно також може поділяти "часткові" підтерміни, що може означати цикли в графічному поданні.

Щоб побачити різницю між цими двома рівнями спільного використання, розгляньте термін

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

Якщо ваш обмін обмежений до завершення підтермінів, як це відбувається в Haskell, у вас може виникнути лише одне виникнення \z.z, але два бета-редексе тут будуть різними: один є, (\z.z) xа другий - (\z.z) ((\z.z) x)і оскільки вони не є рівними умовами їх не можна ділити. Якщо обмін частковими підтермінами дозволений, тоді стає можливим поділити частковий термін (\z.z) [](це не просто функція \z.z, а "функція, \z.zзастосована до чогось ), яка оцінює за один крок просто щось , незалежно від цього аргументу. ви можете мати графік, у якому лише один вузол представляє два програми\z.zдо двох чітких аргументів, і в яких ці два додатки можна звести лише за один крок. Зауважте, що на цьому вузлі існує цикл, оскільки аргументом "першого виникнення" є саме "другий випадок". Нарешті, при оптимальному обміні ви можете перейти від (графік, що представляє) \x. (\z.z) ((\z.z) x))до (графік, що представляє) результат \x.xлише за один крок бета-скорочення (плюс деякий облік). Це в основному те, що відбувається у вашому оптимальному оцінювачі (а представлення графіків - це також те, що запобігає космічному вибуху).

Для дещо розширених пояснень ви можете ознайомитись із документом Слабка оптимізація та значенням спільного доступу (що вас цікавить - це вступ та розділ 4.1, а може бути і деякі бібліографічні вказівки наприкінці).

Повертаючись до вашого прикладу, кодування арифметичних функцій, що працюють над цілими числами Церкви, є одним із "відомих" мін прикладів, коли оптимальні оцінювачі можуть працювати краще, ніж основні мови (у цьому реченні загальновідоме насправді означає, що жменька фахівцям відомі ці приклади). Щоб отримати більше таких прикладів, подивіться статтю Безпечні оператори: Кронштейни закриті назавжди від Asperti та Chroboczek (і, до речі, ви знайдете тут цікаві лямбда-терміни, які не підлягають EAL, тому я рекомендую вам взяти участь погляд на оракули, починаючи з цього паперу Asperti / Chroboczek).

Як ви самі сказали, такий вид кодування є абсолютно непрактичним, але вони все ще являють собою приємний спосіб зрозуміти, що відбувається. І дозвольте мені закінчити виклик для подальшого дослідження: чи зможете ви знайти приклад, на якому оптимальне оцінювання цих нібито поганих кодувань насправді відповідає традиційній оцінці на розумному представленні даних? (наскільки я знаю, це справжнє відкрите питання).


34
Це найбільш незвичайно ретельне перше повідомлення. Ласкаво просимо до StackOverflow!
dfeuer

2
Нічого менш проникливого. Дякую, і ласкаво просимо до громади!
MaiaVictor

7

Це не привид, але це пропозиція, де ви можете почати шукати.

Існує тривіальний спосіб обчислити модульні експоненції в невеликому просторі, зокрема шляхом переписування

(a * x ^ y) % z

як

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

Якщо оцінювач оцінить так і підтримує параметр накопичення aв нормальній формі, тоді ви не будете використовувати занадто багато місця. Якщо дійсно ваш оцінювач є оптимальним, то, мабуть, він не повинен виконувати більше роботи, ніж цей, тому, зокрема, не може використовувати більше місця, ніж час, який цей оцінює.

Я не дуже впевнений, що насправді є оптимальним оцінювачем, тому боюся, що не можу зробити це більш суворим.


4
@Viclib Фібоначчі, як говорить @Tom, є хорошим прикладом. fibвимагає експоненціального часу наївним способом, який можна звести до лінійного простого запам'ятовування / динамічного програмування. Навіть логарифмічний (!) Час можливий через обчислення потужності n-ї матриці [[0,1],[1,1]](до тих пір, поки ви порахуєте, що кожне множення має постійну вартість).
чі

1
Навіть постійний час, якщо ти досить
Дж. Абрахамсон,

5
@TomEllis Чому щось, що знає лише, як зменшити довільні вирази лямбдального числення, має таке уявлення (a * b) % n = ((a % n) * b) % n? Це таємнича частина, безумовно.
Рейд Бартон

2
@ReidBarton, безумовно, я спробував це! Хоча однакові результати.
MaiaVictor

2
@TomEllis та Chi, хоча є лише невеличке зауваження. Це все припускає, що традиційна рекурсивна функція - це "наївна" реалізація фіб, але для ІМО існує альтернативний спосіб її вираження, що набагато природніше. Нормальна форма цього нового представлення має половину розміру від традиційного), і Optlam вдається обчислити це лінійно! Тому я б заперечував, що це "наївне" визначення фіб, що стосується λ-обчислення. Я б
написав
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.