Реализация бесконечного списка с последовательностью для улучшения временной сложности в Haskell

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

Например, я использую следующую функцию

countup n = n : countup (n+1)

И пытаюсь получить [1, 2, 3, 4...] (начиная с 1, просто увеличивая 1 и добавляя в список) как бесконечный ленивый список. Как мне это сделать?

ОБНОВЛЕНО:

Я пытаюсь заставить (взять k (счетчик 0)) использовать пространство O (1). Здесь функция взятия выглядит следующим образом:

take k (x:xl) = 
    if k==0
    then x
    else take (k-1) xl

Для этого не нужно seq. Особенно, если вы хотите, чтобы ваш список был ленивым! Просто сделай countup n = [n..].

bradrn 21.12.2020 06:17

Что касается изучения того, как работает seq, вот несколько полезных статей, которые вы, возможно, захотите прочитать: wiki.haskell.org/Seq stackoverflow.com/questions/11046590/…

bradrn 21.12.2020 06:19

@bradrn Я хочу минимизировать использование пространства с помощью seq. Поэтому я реализовал функцию, которая принимает число (индекс) и список (бесконечный список), который находит элемент индекса бесконечного списка. И я слышал, что я могу каким-то образом минимизировать пространственную сложность до O (1), если я использую seq для реализации списка?

Sod 21.12.2020 06:49

В общем, в Haskell очень сложно рассуждать о пространственной сложности. Если ваша функция использует больше места, чем ожидалось, seq может помочь уменьшить его, но только если вы используете его правильно. С какой функцией вы пытаетесь его использовать?

bradrn 21.12.2020 07:23

@James: ленивые функции часто также могут сокращать пространство. Действительно, [1..n] занимает, если он остается невычисленным, только O (1) пространства, и только если вы «потребляете» список, он расширяется, но даже тогда не сам по себе до n элементов, поэтому ленивое вычисление часто может сэкономить память как хорошо.

Willem Van Onsem 21.12.2020 13:39

@bradrn Я обновил код, чтобы свести к минимуму космическую сложность

Sod 21.12.2020 20:05
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
6
192
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
countup n = n : countup (n+1)

Каждый элемент списка создается как преобразователь previousElement + 1. Итак, если вы take 1000000-й или какой-то другой элемент, это будет очень большой преобразователь (...(n + 1)... + 1), содержащий ~ 1000000 приостановок. Несмотря на то, что :-ячейки могут быть GC'ированы, как только они будут созданы (поэтому обход самого корешка списка занимает O(1) места), сами элементы дублируют структуру списка и поэтому take k (countup n) по-прежнему занимают O(k) место.

Нам бы хотелось, чтобы при оценке ячеек : также оценивались элементы. Мы можем сделать это с

countup n = seq n $ n : countup (n + 1)

Теперь, при оценке seq n $ n : countup (n + 1), seq вызовет оценку как n, так и n : countup (n + 1). Вычисление последнего ничего не делает (оно уже вычислено), а вычисление первого выполняет любое thunked добавление, так что + 1s никогда не накапливаются. С этим определением take k (countup n) занимает O(1) место (или, на самом деле, O(log(n + k))).

Мы также можем записать улучшенную функцию как

countup !n = n : countup (n + 1)

Я попытался профилировать обе версии countup в качестве эксперимента, но не вижу разницы между ними ни в использовании памяти, ни в скорости. Вы уверены, что реализация с seq занимает меньше места, чем без?

bradrn 22.12.2020 03:22

Да, совершенно уверен. Скомпилируйте {-# LANGUAGE BangPatterns #-} module Main where import Prelude hiding (take); countUp !x = x : countUp (x + 1); take 0 (x : _) = x; take n (_ : xs) = take (n - 1) xs; main = print =<< (flip take (countUp 0) <$> readLn) с ghc -O. Затем запустите его с помощью ./Main +RTS -s. Дайте входные данные 1000, 1000000, 10000000: вывод (для меня) показывает использование кучи 2 МБ. Теперь удалите ! и перекомпилируйте. Передача 1000 по-прежнему использует ~ 2 МБ. Прохождение 1000000 занимает ~67 МБ, а 10000000 ~670 МБ. Ой! Не знаю, что вы сделали, но обратите внимание, что «профилирование» может дать неправильные результаты и не является необходимым.

HTNW 22.12.2020 03:28

Еще один способ написать это: (!:) :: a -> [a] -> [a]; a !: as = a `seq` a : as, затем countup n = n !: countup (n + 1).

dfeuer 22.12.2020 04:09

Хорошо @HTNW, я попробую и посмотрю, сработает ли это. (Хм… может быть, наша разница в результатах была из-за того, что мы использовали разные параметры GHC? Я скомпилировал с -prof, а не -O, чтобы я мог создать профиль кучи.)

bradrn 22.12.2020 04:33

Спасибо! Я получил первую версию countup countup как countup n = n seq n : countup (n + 1), но я не был уверен. И спасибо за дополнительную информацию о том, как проверить использование кучи :) мило. @HTNW.

Sod 22.12.2020 04:36

@HTNW Нет, я не могу повторить ваши результаты: я получаю 0,2 МБ за 1000, 80 МБ за 1000000, 800 МБ за 10000000. (См. pastebin.com/DBNiQMZK для вывода.)

bradrn 22.12.2020 04:45

@bradrn Вы смотрите не на тот номер. «Общее количество выделений» не является мерой использования памяти; это мера работы. Он подсчитывает каждый выделенный объект, но не отсчитывает освобождение. Максимальное фактическое использование памяти в данный момент времени — это «общая используемая память», которая, как вы видите, не меняется (показывает 0, ха!). т.е. да, улучшенный countup строит и прожигает много-много объектов, но эти объекты разделены во времени; они не занимают 800 МБ фактической памяти (в этом и есть смысл: поведение O(1) связано с тем, что «пропущенные» ячейки GC сразу после создания).

HTNW 22.12.2020 05:16

@bradrn если я прохожу 1 000 000 000, я получаю ~ 80 ГБ «Общих выделений» ... у картофеля, на котором я его запускаю, 4 ГБ ОЗУ. Если вы попробуете нестрогий вариант, он на самом деле попытается израсходовать все эти куски и куски памяти... Забавно, но кажется, что общее количество выделений примерно в два раза превышает максимальное использование памяти в этом случае, что, я думаю, может соответствовать на наличие одного сохраненного объекта (преобразователь n + 1) на два объекта (ячейка : и преобразователь).

HTNW 22.12.2020 05:39

@HTNW Спасибо за исправление! Очевидно, я понятия не имел, как читать этот вывод… это действительно поможет в следующий раз, когда мне нужно будет выяснить, как программа использует кучу.

bradrn 22.12.2020 05:52

Другие вопросы по теме