Iterate выдает ошибки StackOverflow

Так что я только начал с Frege и Haskell. У меня есть опыт работы с функциональными языками, так как я использую Clojure уже пару лет. Первое, что я хотел попробовать, это мой обычный подход к числам Фибоначчи.

next_fib (a, b) = (b, a + b)
fibs = map fst $ iterate next_fib (0, 1)
fib x = head $ drop x fibs

Так получилось у Фреге. Это работает, но для очень больших чисел для вымысла, например. (fib 4000), выдает ошибки StackOverflow. Меня это удивило, потому что те же самые функции в Clojure будут работать нормально. Это ошибка Фреге или я неправильно понимаю всю ленивую оценку?


person Nathan    schedule 25.08.2015    source источник
comment
Можете ли вы изменить , перед a+b на ; и попробуй еще раз? Объяснение последует позже.   -  person Ingo    schedule 26.08.2015
comment
Изменения: next_fib (a, b) = (b ; a + b) По-прежнему выдает: Exception in thread main java.lang.StackOverflowError at frege.runtime.Fun1$1.eval(Fun1.java:63) at frege.runtime.Delayed .call(Delayed.java:198) в frege.runtime.Delayed.forced(Delayed.java:257) в Main$?$next_fibƒ5956ce1a.eval(Main.java:190) в Main$?$next_fibƒ5956ce1a.eval(Main. Ява: 1)   -  person Nathan    schedule 26.08.2015
comment
Я также пробовал код в ghci, и там он работает просто отлично.   -  person Nathan    schedule 26.08.2015
comment
Жалко, надо будет разобраться. Haskell имеет гораздо больший стек, чем JVM, вы можете попробовать -Xss32m или что-то в этом роде, и это тоже должно быть хорошо. Но, конечно, это не универсальное решение.   -  person Ingo    schedule 26.08.2015
comment
Теперь он работает, если я добавлю -Xss4m в конфигурацию запуска в eclipse, только добавления его в файл eclipse.ini явно недостаточно. Теперь только StackOverflows (фиб 40000). Этот работает с -Xss32m. Большое спасибо за помощь. Я с нетерпением жду возможности узнать больше о Haskell и Frege.   -  person Nathan    schedule 26.08.2015
comment
Позже я опубликую решение, которое работает без дополнительного стека, и объясню, что происходит.   -  person Ingo    schedule 26.08.2015


Ответы (1)


Вы, вероятно, не «неправильно понимаете всю ленивую оценку», но в этом случае вы дважды укушены слишком ленивой оценкой.

И хотя в этом отношении ГХК работает точно так же, как и Фреге, результат получается другим и, по-видимому, неблагоприятным для Фреге.

Но причина того, что Haskell может работать с действительно большими переходами [см. ниже], в то время как Frege преждевременно завершает работу с переполнением стека, заключается в том, как системы времени выполнения управляют кучей и стеком. Haskell RTS является гибким и может выделить для стека огромные части доступной памяти, если возникнет такая необходимость. В то время как исполняющая система Фреге представляет собой JVM, которая обычно начинается с крошечного стека, достаточного для обработки глубины вызовов в несколько сотен. Как вы заметили, предоставление JVM достаточного стекового пространства заставляет думать так же, как и в GHC.

Из-за нехватки места в стеке в JVM мы разработали некоторые методы во Фреге, чтобы избежать нежелательной и ненужной лени. Два из них будут описаны ниже. В конце концов, во Фреге вы вынуждены заранее контролировать негативные последствия лени, в то время как разработчик GHC может с радостью писать код, не обращая на это внимания.

Чтобы понять следующее, нам необходимо ввести понятие «thunk». Преобразователь - это, прежде всего, выражение, которое еще предстоит оценить. Например, поскольку кортежи ленивы, такое выражение, как

(b, b+a)

компилируется в приложение конструктора кортежей от (,) до b и {a+b}, где обозначение { e } для целей этого обсуждения означает некоторое зависимое от реализации представление thunk, которое обещает вычислить выражение e при оценке. Кроме того, преобразователь запоминает свой результат при оценке, поэтому всякий раз, когда тот же преобразователь оценивается снова, он просто возвращает предварительно вычисленный результат. (Конечно, это возможно только в чистом функциональном языке.)

Например, во Фреге для представления санков есть класс Delayed<X>, реализующий Callable<X> и обеспечивающий запоминание результата.

Теперь мы будем исследовать, каков результат

next_fib (next_fib (0, 1)) 

является. Внутреннее приложение приводит к:

(1, {0+1})

а затем внешний вычисляет из этого:

({0+1}, {1+{0+1}})

Здесь мы видим, что санки могут вкладываться в другие санки, и в этом заключается проблема, поскольку каждое применение next_fib приведет к созданию кортежа, в котором в качестве элементов будут санки, внутри которых вложены санки предыдущей итерации.

Теперь рассмотрим, что происходит, когда вычисляется преобразователь для 4000-го числа выдумки, что происходит, например, когда вы печатаете его. Он должен будет выполнить сложение, но добавляемые числа на самом деле являются преобразовательами, которые должны быть оценены, прежде чем сложение может иметь место. Таким образом, каждый вложенный преобразователь означает вызов метода оценки этого переходника, если только преобразователь уже не оценен. Следовательно, чтобы напечатать 4000-е число, нам нужна глубина стека не менее 4000 в случае, когда никакой другой преобразователь этой серии ранее не вычислялся.

Итак, первой мерой была замена ленивого конструктора кортежей на строгий:

(b; a+b)

Он не создает преобразователи, а сразу вычисляет аргументы. Это недоступно в Haskell, чтобы сделать то же самое, вам нужно сказать что-то вроде:

let c = a+b in b `seq` c `seq` (b,c)

Но это был не конец истории. Оказалось, что вычисление fib 4000 по-прежнему переполняло стек.

Причина в реализации iterate, которая выглядит следующим образом:

iterate f x = x : iterate f (f x)

Это создает бесконечный список

[ x, f x, f (f x), f (f (f x)), ...]

Излишне говорить, что все термины, кроме первого, являются переходниками!

Обычно это не проблема, когда элементы списка оцениваются в последовательном порядке, потому что когда, например, 3-й член

{f {f x}}

оценивается, внутренний преобразователь выполнен уже вычислен и сразу же возвращает результат. В общем, нам нужна только достаточная глубина стека, чтобы достичь первого ранее вычисленного члена. Вот демонстрация прямо из онлайн-REPL frege на try.frege-lang.org.

frege> next (a,b) = (b; a+b) :: (Integer, Integer)
function next :: (Integer,Integer) -> (Integer,Integer)
frege> fibs = map fst $ iterate next (0,1)
function fibs :: [Integer]
frege> fib = (fibs!!)
function fib :: Int -> Integer
frege> map (length . show . fib) [0,500 ..]
[1,105,209,314,418,523,627,732,836,941,1045,1150,...]
frege> fib 4000
39909473435004422792081248094960912600792...

Здесь с помощью карты мы принудительно вычисляем каждое 500-е число (поскольку REPL требует вывода, он будет печатать только начальные части бесконечных списков) и вычисляем длину десятичного представления каждого числа (просто чтобы не отображать большие полученные числа). Это, в свою очередь, вызывает оценку 500 предыдущих чисел, но это нормально, так как для этого достаточно места в стеке. Как только это будет сделано, мы сможем вычислить даже fib 4000! Потому что теперь все преобразователи до 6000 уже оценены.

Но мы можем сделать еще лучше с немного улучшенной версией iterate, которая использует строгий конструктор головы (!:):

a !: as = a `seq` (a:as)

Это сразу оценивает голову списка, что уместно в нашем случае.

С этими двумя изменениями мы получаем программу, требования к стеку которой больше не зависят от аргумента fib. Вот доказательство:

frege> iterate' f x = x !: iterate' f (f x)
function iterate' :: (a->a) -> a -> [a]
frege> fibs2 = map fst $ iterate' next (0,1)
function fibs2 :: [Integer]
frege> (length . show . (fibs2 !!)) 4000
836
frege> (length . show . (fibs2 !!)) 8000
1672
frege> (length . show . (fibs2 !!)) 16000
3344
frege> (length . show . (fibs2 !!)) 32000
6688
frege> (length . show . (fibs2 !!)) 64000
13375
frege> (length . show . (fibs2 !!)) 128000
java.lang.OutOfMemoryError: Java heap space

Что ж, теперь нам потребуется больше пространства в куче, чтобы хранить более 100 000 огромных чисел. Но обратите внимание, что больше не было проблемы со стеком для вычисления 32 000 новых чисел на последнем шаге.

Мы могли бы избавиться от проблемы пространства в куче с помощью простого рекурсивного определения хвоста, которому не нужно отмечать все эти числа:

fib :: Int -> Integer
fib n = go n 0 1 where
    go :: Int -> Integer -> Integer -> Integer
    go 0 !a !b = a
    go n !a !b = go (n-1) b (a+b)

Я думаю, это будет даже быстрее, чем обход списка.

В отличие от (?) в Clojure, прямой доступ к списку — O(n), а длинные списки занимают много места. Поэтому, если вам нужно что-то кешировать и иметь верхний предел, вам лучше использовать массивы. Вот 2 способа построить массив из 10000 фибов:

frege> zzz = arrayFromList $ take 10000 $ map fst $ iterate (\(a,b) -> (b; a+b)) (0n,1)
function zzz :: JArray Integer
frege> elemAt zzz 4000
39909473435004422792081248094960912600792570982820257 ...

Это работает, потому что промежуточный список никогда не должен существовать как единое целое. И после создания доступ O (1)

А еще есть специальная функция для создания таких кешей:

yyy = arrayCache f 10000 where 
       f 0 a = 0n
       f 1 a = 1n
       f n a = elemAt a (n-1) + elemAt a (n-2)
fib = elemAt yyy

Это позволяет избежать даже промежуточного списка, всех кортежей и так далее.

Таким образом, вы сохраните свою добрую привычку предпочитать комбинаторы явной рекурсии. Пожалуйста, попробуйте.

person Ingo    schedule 26.08.2015
comment
Лучшее объяснение! - person Dierk; 26.08.2015
comment
Спасибо за подробный ответ. Это определенно помогло. Думаю, я слишком привык избегать рекурсии в clojure с помощью функции итерации. Теперь я знаю, что не могу просто применять свои старые техники в frege и по-прежнему ожидать тех же результатов. Ну, я думаю, что это обратно к рекурсиям для меня. С нетерпением жду создания более крупных программ, теперь, когда это не так. - person Nathan; 27.08.2015
comment
@ Натан, мне должно быть жаль это слышать. Наоборот, рекомендуется использовать комбинаторы, такие как interate, map и т. д. (Мы добавим iterate' в stdlib.) Но помимо этого, огромные списки не являются идеальными кэшами ни в Haskell, ни в Frege из-за доступа O(n) и сравнительно большого объема памяти. Таким образом, я добавил абзац, в котором перечислены альтернативы. - person Ingo; 27.08.2015
comment
Можем ли мы избежать проблемы с кучами, изменив реализацию списка? Предположим, что мы (удаляем 40000 xs), не будут ли первые 40000 элементов доступны для сборки мусора, если xs будет строго оцениваться с помощью iterate' ? Если бы мы не держались за начало списка и он собирал бы мусор при каждом удалении, могло бы это заставить итерацию работать с бесконечными списками? - person Nathan; 27.08.2015
comment
О да, только что попробовал. Отлично работает так: next_fib (a, b) = (b; a + b) iterate' fx = x !: iterate' f (fx) fib1 x = fst $ head $ drop x $ iterate' next_fib (0; 1) без взрыва кучи или стека - person Nathan; 27.08.2015