Почему оптимальные оценщики λ-исчисления способны вычислять большие модульные возведения в степень без формул?


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

С моим «предупреждением об ошибке» я пошел в Google и проверил, 10^10%13 == 3действительно. Но λ-калькулятор не должен был найти этот результат, он едва может хранить 10 ^ 10. Я начал подчеркивать это для науки. Он немедленно ответил мне 20^20%13 == 3, 50^50%13 == 4, 60^60%3 == 0. Мне пришлось использовать внешние инструменты для проверки этих результатов, поскольку сам Haskell не смог его вычислить (из-за целочисленного переполнения) (конечно, если вы используете целые числа, а не целые числа!). Раздвинув его до предела, это было ответом на 200^200%31:

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

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

Если бы у нас была одна копия вселенной для каждого атома во вселенной, и у нас был компьютер для каждого атома, который у нас был в сумме, мы не смогли бы сохранить номер церкви 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)

Это правильно выводит 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

Это явление происходит от количества общих шагов бета-сокращения, которые могут существенно отличаться в ленивой оценке в стиле Хаскеля (или обычном вызове по значению, которое не так уж далеко в этом отношении) и в Vuillemin-Lévy-Lamping- 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, и, возможно, некоторые из библиографических указателей в конце).

Возвращаясь к вашему примеру, кодирование арифметических функций, работающих с целыми числами Чёрча, является одним из «хорошо известных» примеров, когда оптимальные оценщики могут работать лучше, чем обычные языки (в этом предложении общеизвестный означает, что несколько специалисты знают об этих примерах). Дополнительные примеры можно найти в статье « Безопасные операторы: закрытые скобки навсегда » Асперти и Хробочека (и, между прочим, здесь вы найдете интересные лямбда-термины, которые не допускают EAL-типа; поэтому я призываю вас принять взгляд на оракулы, начиная с этой статьи Асперти / Хробочека).

Как вы сказали сами, этот вид кодирования совершенно непрактичен, но он все еще представляет собой хороший способ понять, что происходит. И позвольте мне в заключение ответить на вопрос, требующий дальнейшего изучения: сможете ли вы найти пример, на котором оптимальная оценка этих предположительно плохих кодирований фактически соответствует традиционной оценке разумного представления данных? (насколько я знаю, это действительно открытый вопрос).


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
Даже постоянное время, если вы достаточно смелы, чтобы приблизиться :)
J. Abrahamson

5
@TomEllis Почему кто-то, кто знает только, как уменьшить произвольные выражения лямбда-исчисления, имеет хоть какую-то идею (a * b) % n = ((a % n) * b) % n? Это таинственная часть, конечно.
Рейд Бартон

2
@ReidBarton, конечно, я попробовал это! Те же результаты, хотя.
MaiaVictor

2
@TomEllis и Chi, есть только небольшое замечание. Все это предполагает, что традиционная рекурсивная функция является «наивной» реализацией fib, но у IMO есть альтернативный способ выразить это, который является гораздо более естественным. Нормальная форма этого нового представления имеет половину размера традиционного), и Optlam удается вычислить это линейно! Так что я бы сказал, что это «наивное» определение fib в отношении λ-исчисления. Я сделал бы сообщение в блоге, но я не уверен, что это действительно стоит того ...
MaiaVictor
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.