Ответ @ sacundim в основном верен, но есть и другие важные идеи о компромиссе между языком и практическими требованиями.
Объекты и ссылки
Эти языки обычно предписывают (или предполагают) объекты, имеющие несвязанные динамические экстенты (или, на языке C, время жизни , хотя не совсем одно и то же из-за различий в значении объектов в этих языках, см. Ниже) по умолчанию, избегая ссылок первого класса ( например, указатели объектов в C) и непредсказуемое поведение в семантических правилах (например, неопределенное поведение ISO C, связанное с семантикой).
Кроме того, понятие (первоклассных) объектов в таких языках консервативно ограничительно: по умолчанию ничего «локативных» свойств не указывается и не гарантируется. Это полностью отличается в некоторых языках, подобных ALGOL, чьи объекты не имеют несвязанных динамических экстентов (например, в C и C ++), где объекты в основном означают некоторые виды «типизированного хранилища», обычно связанные с областями памяти.
Кодирование хранилища внутри объектов имеет некоторые дополнительные преимущества, такие как возможность добавлять детерминированные вычислительные эффекты в течение всего срока их службы, но это другая тема.
Проблемы моделирования структур данных
Без первоклассных ссылок односвязные списки не могут эффективно и переносимо моделировать многие традиционные (нетерпеливые / изменяемые) структуры данных из-за характера представления этих структур данных и ограниченных примитивных операций в этих языках. (Напротив, в C вы можете легко получить связанные списки даже в строго соответствующей программе .) И такие альтернативные структуры данных, как массивы / векторы, действительно имеют некоторые превосходные свойства по сравнению с односвязными списками на практике. Вот почему R 5 RS вводит новые примитивные операции.
Но существуют различия между типами векторов / массивов и списками с двойной связью. Массив часто предполагается с O (1) сложностью времени доступа и меньшими накладными расходами пространства, которые являются превосходными свойствами, не разделяемыми списками. (Хотя, строго говоря, ни один из них не гарантируется ISO C, но пользователи почти всегда ожидают этого, и никакая практическая реализация не нарушит эти неявные гарантии слишком очевидно.) OTOH, список с двойной связью часто делает оба свойства даже хуже, чем список с одной ссылкой тогда как обратная / прямая итерация также поддерживается массивом или вектором (вместе с целочисленными индексами) с еще меньшими издержками. Таким образом, двусвязный список не работает лучше в целом. Еще хуже, производительность по эффективности кэша и задержке при динамическом распределении памяти по спискам катастрофически хуже, чем производительность по массивам / векторам при использовании распределителя по умолчанию, предоставляемого базовой средой реализации (например, libc). Таким образом, без очень специфичной и «умной» среды выполнения, которая сильно оптимизирует создание таких объектов, типы массивов / векторов часто предпочтительнее связанных списков. (Например, при использовании ISO C ++ есть оговорка,std::vector
должно быть предпочтительнее std::list
по умолчанию.) Таким образом, введение новых примитивов для конкретной поддержки (дважды) связанных списков определенно не так выгодно, как поддержка структур массивов / векторных данных на практике.
Справедливости ради, списки по-прежнему имеют некоторые специфические свойства лучше, чем массивы / векторы:
- Списки основаны на узлах. Удаление элементов из списков не делает недействительной ссылку на другие элементы в других узлах. (Это также верно для некоторых древовидных или графических структур данных.) OTOH, массивы / векторы могут делать ссылки на аннулируемую конечную позицию (с массивным перераспределением в некоторых случаях).
- Списки могут склеиваться за O (1) раз. Реконструкция новых массивов / векторов с текущими намного дороже.
Однако эти свойства не слишком важны для языка со встроенной поддержкой односвязных списков, который уже может использоваться таким образом. Хотя все еще существуют различия, в языках с обязательными динамическими экстентами объектов (что обычно означает, что существует сборщик мусора, скрывающий висячие ссылки), аннулирование также может быть менее важным, в зависимости от намерений. Итак, единственными случаями, когда выигрывают двусвязные списки, могут быть:
- Требуются как гарантия отсутствия перераспределения, так и требования к двунаправленной итерации. (Если важна производительность доступа к элементам и набор данных достаточно большой, я бы вместо этого выбрал бинарные деревья поиска или хеш-таблицы.)
- Необходимы эффективные двунаправленные операции сращивания. Это значительно редко. (Я только отвечаю требованиям только для реализации чего-то вроде линейных записей истории в браузере.)
Неизменность и алиасинг
На чистом языке, таком как Haskell, объекты неизменны. Объект схемы часто используется без мутаций. Такой факт позволяет эффективно повысить эффективность памяти за счет интернирования объектов - неявного совместного использования нескольких объектов с одинаковым значением на лету.
Это агрессивная стратегия оптимизации высокого уровня в дизайне языка. Однако это связано с проблемами реализации. Это фактически вводит неявные псевдонимы в базовые ячейки памяти. Это затрудняет анализ псевдонимов. В результате, вероятно, будет меньше возможностей для устранения издержек, связанных с ссылками не первого класса, даже пользователи никогда их не трогают. В таких языках, как Scheme, когда мутация не полностью исключена, это также мешает параллелизму. Это может быть хорошо на ленивом языке (у которого уже есть проблемы с производительностью, вызванные thunks в любом случае), все же.
Для программирования общего назначения такой выбор языкового дизайна может быть проблематичным. Но с некоторыми общими шаблонами функционального кодирования языки, кажется, все еще работают хорошо.