Почему списки минусов связаны с функциональным программированием?

22

Я заметил, что большинство функциональных языков используют односвязный список (список «минусов») в качестве наиболее фундаментальных типов списков. Примеры включают Common Lisp, Haskell и F #. Это отличается от основных языков, где родные типы списков являются массивами.

Почему это?

Для Common Lisp (будучи динамически типизированным) я понял, что минусы достаточно общие, чтобы также быть основой списков, деревьев и т. Д. Это может быть крошечной причиной.

Однако для статически типизированных языков я не могу найти веских аргументов, я даже могу найти контраргументы:

  • Функциональный стиль поощряет неизменность, поэтому простота вставки связанного списка является меньшим преимуществом,
  • Функциональный стиль поощряет неизменность, а также обмен данными; массив легче разделить «частично», чем связанный список,
  • Вы можете сделать сопоставление с образцом для обычного массива точно так же, и даже лучше (например, вы можете легко сложить справа налево),
  • Кроме того, вы получаете произвольный доступ бесплатно,
  • И (практическое преимущество), если язык статически типизирован, вы можете использовать обычную структуру памяти и получить повышение скорости из кеша.

Так почему же предпочитаете связанные списки?

Кос
источник
4
Исходя из комментариев к ответу @ sepp2k, я думаю, что an array is easier to share "partially" than a linked listнужно уточнить, что вы имеете в виду. Из-за их рекурсивного характера, как я понимаю, верно обратное: вы можете частично поделиться связанным списком, передавая любой узел в нем, в то время как массиву потребуется потратить время на создание новой копии. Или с точки зрения совместного использования данных, два связанных списка могут указывать на один и тот же суффикс, что просто невозможно с массивами.
Изката
Если массив определяет себя как смещение, длину, буферную тройку, то вы можете разделить массив, создав новый с буфером смещения + 1, длина-1. Или иметь специальный тип массива в качестве подмассива.
Добес Вандермер
@Izkata Говоря о массивах, мы редко имеем в виду буфер, такой как указатель на начало непрерывной памяти в C. Обычно мы имеем в виду некую структуру, которая хранит длину и указатель на начало упакованного буфера. В такой системе операция среза может возвращать подмассив, указатель буфера которого указывает на середину буфера (на первый элемент вложенного массива), и чье количество таково, что start + count дает вам последний элемент. Такие операции нарезки O (1) во времени и пространстве
Александр - Восстановить Монику

Ответы:

22

Наиболее важным фактором является то, что вы можете добавить к неизменяемому односвязному списку за O (1) время, что позволяет рекурсивно создавать n-элементные списки за O (n) времени следующим образом:

// Build a list containing the numbers 1 to n:
foo(0) = []
foo(n) = cons(n, foo(n-1))

Если вы сделали это с использованием неизменяемых массивов, время выполнения будет квадратичным, потому что каждая consоперация должна будет копировать весь массив, что приведет к квадратичному времени выполнения.

Функциональный стиль поощряет неизменность, а также обмен данными; массив легче разделить "частично", чем связанный список

Я предполагаю, что под «частичным» разделением вы подразумеваете, что вы можете взять подмассив из массива за O (1) время, тогда как со связанными списками вы можете взять хвост только за O (1), а все остальное нуждается в O (n). Это правда.

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

sepp2k
источник
Это совсем не так. Вы можете добавлять к массивам в амортизированной O (1).
DeadMG
10
@DeadMG Да, но не для неизменяемых массивов.
сентября
«частично совместное использование» - я думаю, что оба списка минусов могут указывать на один и тот же список суффиксов (не знаю, зачем вам это нужно), и что вы можете передать среднюю точку вместо начала списка другой функции без необходимости копировать это (я делал это много раз)
Изката
@Izkata ОП говорил о частичном совместном использовании массивов, а не списков. Кроме того, я никогда не слышал, что вы описываете как частичный обмен. Это просто поделиться.
sepp2k
1
@Izkata ОП использует термин «массив» ровно три раза. Однажды сказать, что языки FP используют связанные списки, в то время как другие языки используют массивы. Однажды сказать, что массивы лучше при частичном совместном использовании, чем связанные списки, и еще раз сказать, что массивы можно сопоставлять с образцом точно так же (как связанные списки). Во всех случаях он противопоставляет массивы и связанные списки (чтобы подчеркнуть, что массивы будут более полезны в качестве первичной структуры данных, чем связанные списки, что приводит к его вопросу, почему связанные списки предпочтительнее в FP), поэтому я не вижу, как он могли бы использовать термины взаимозаменяемо.
сентября
4

Я думаю, что все сводится к тому, что списки довольно легко реализуются в функциональном коде.

Схема:

(define (cons x y)(lambda (m) (m x y)))

Haskell:

data  [a]  =  [] | a : [a]

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

Кроме того, рекурсия работает намного лучше для списков, чем для массивов. Подумайте, сколько раз вы рекурсивно использовали / генерировали список по сравнению с индексированным массивом.

Pubby
источник
Я бы не сказал, что правильно называть версию вашей схемы реализацией связанных списков. Вы не сможете использовать его для хранения чего-либо, кроме функций. Также массивы сложнее (на самом деле невозможно) реализовать на любом языке, который не имеет встроенной поддержки для них (или блоков памяти), в то время как для связанных списков требуется только что-то вроде структур, классов, записей или алгебраических типов данных для реализации. Это не относится к функциональным языкам программирования.
sepp2k
@ sepp2k Что значит "хранить что-либо, кроме функций" ?
Пабби
1
Я имел в виду, что списки, определенные таким образом, не могут хранить ничего, кроме функции. Это на самом деле не правда, хотя. Не знаю, почему я так думал. Прости за это.
sepp2k
2

Односвязный список - это самая простая постоянная структура данных .

Постоянные структуры данных необходимы для производительного, чисто функционального программирования.

ziggystar
источник
1
это, кажется, просто повторяет высказанное и объясненное в верхнем ответе, который был опубликован более 4 лет назад
комнат
3
@gnat: в верхнем ответе не упоминаются постоянные структуры данных, или то, что односвязные списки являются простейшей постоянной структурой данных, или что они необходимы для производительного чисто функционального программирования. Я не могу найти совпадения с главным ответом вообще.
Майкл Шоу
2

Вы можете легко использовать узлы Cons, только если у вас есть язык для сборки мусора.

Минусы Узлы во многом соответствуют стилю функционального программирования рекурсивных вызовов и неизменяемых значений. Так что это хорошо вписывается в модель умственного программиста.

И не забывайте исторические причины. Почему они по-прежнему называются узлами "минусы", а еще хуже - они используют автомобиль и CDR в качестве аксессуаров? Люди изучают это из учебников и курсов и затем используют это.

Вы правы, в реальном мире массивы намного проще в использовании, они занимают только половину пространства памяти и намного эффективнее из-за пропадания уровня кэша. Нет причин использовать их с обязательными языками.

Лотар
источник
1

Связанные списки важны по следующей причине:

Как только вы возьмете число, подобное 3, и преобразуете его в последовательность-преемник, например succ(succ(succ(zero))), и затем примените к нему замену {succ=List node with some memory space}, и {zero = end of list}вы получите связанный список (длиной 3).

Фактически важная часть - это числа, подстановка, пространство памяти и ноль.

ТР1
источник