Списки в Haskell: тип данных или абстрактный тип данных?
Из того, что я понимаю, тип списка в Haskell реализован внутренне с использованием связанного списка. Однако пользователь языка не видит деталей реализации и не имеет возможности изменять "ссылки", составляющие связанный список, чтобы он мог указывать на другой адрес памяти. Это, я полагаю, сделано внутренне.
Как тогда, может ли тип списка быть квалифицированным как в Haskell? Является ли это "типом данных" или "абстрактным типом данных"? А что из типа связанного списка реализации?
Кроме того, поскольку тип списка, предоставляемый прелюей, не является типом связанного списка, как можно реализовать основные функции связанных списков?
Возьмем, к примеру, эту часть кода, предназначенную для добавления элемента a в индекс n списка:
add [] acc _ _ = reverse acc
add (x:xs) acc 0 a = add xs (x:a:acc) (-1) a
add (x:xs) acc n a = add xs (x:acc) (n-1) a
Используя "реальный" связанный список, добавление элемента будет состоять только в изменении указателя на адрес памяти. Это невозможно в Haskell (или это?), Поэтому вопрос: является ли моя реализация добавлением элемента в список наилучшим, или я чего-то не хватает (использование функции reverse
есть, я думаю, особенно уродливые, но можно ли обойтись без?)
Пожалуйста, не стесняйтесь корректировать меня, если что-либо, что я сказал, неправильно, и спасибо за ваше время.
Ответы
Ответ 1
Вы смешиваете изменчивость с структурой данных. Это правильный список - просто не тот, который вам разрешено изменять. Haskell является чисто функциональным, то есть значения постоянны - вы не можете изменить элемент в списке больше, чем можете превратить число 2 в 3. Вместо этого вы выполняете вычисления для создания новых значений с желаемыми изменениями.
Вы можете определить эту функцию наиболее просто следующим образом:
add ls idx el = take idx ls ++ el : drop idx ls
Список el : drop idx ls
повторно использует хвост исходного списка, поэтому вам нужно только создать новый список до idx
(что делает функция take
). Если вы хотите сделать это с помощью явной рекурсии, вы можете определить ее так:
add ls 0 el = el : ls
add (x:xs) idx el
| idx < 0 = error "Negative index for add"
| otherwise = x : add xs (idx - 1) el
add [] _ el = [el]
Это повторяет хвост списка таким же образом (что el : ls
в первом случае).
Поскольку у вас, похоже, не получается увидеть, как это связанный список, дайте понять, что такое связанный список: это структура данных, состоящая из ячеек, где каждая ячейка имеет значение и ссылку на следующий элемент. В C он может быть определен как:
struct ListCell {
void *value; /* This is the head */
struct ListCell *next; /* This is the tail */
}
В Lisp он определяется как (head . tail)
, где head
- это значение, а tail
- ссылка на следующий элемент.
В Haskell он определяется как data [] a = [] | a : [a]
, где a
- это значение, а [a]
- ссылка на следующий элемент.
Как вы можете видеть, эти структуры данных эквивалентны. Единственное отличие состоит в том, что в C и Lisp, которые не являются чисто функциональными, значения головы и хвоста - это вещи, которые вы можете изменить. В Haskell вы не можете их изменить.
Ответ 2
Haskell - это чисто функциональный язык программирования. Это означает, что никаких изменений не может быть сделано вообще.
Списки не являются абстрактными типами, это просто связанный список.
Вы можете думать о них так:
data [a] = a : [a] | []
который является именно тем, как определен связанный список. Элемент head и (указатель на) остальных.
Обратите внимание, что это не является внутренним. Если вы хотите иметь более эффективные типы, используйте Sequence
или Array
. (Но поскольку никаких изменений не разрешено, вам не нужно фактически копировать списки, чтобы различать копии, что может быть усилением производительности, а не императивными языками).
Ответ 3
В Haskell "тип данных" и "абстрактный тип" - это термины:
-
"Тип данных" (который не является абстрактным) имеет конструкторы видимых значений, которые вы можете сопоставлять шаблону в выражениях или определениях функций case
.
-
В "абстрактном типе" нет конструкторов видимых значений, поэтому вы не можете сопоставлять шаблон по значениям типа.
Учитывая тип a
, [a]
(список a
) - это тип данных, потому что вы можете сопоставлять шаблоны с видимыми конструкторами cons (записано :
) и nil (написано []
). Примером абстрактного типа будет IO a
, который вы не можете деконструировать по шаблону.
Ответ 4
Ваш код может работать, но он определенно не оптимален. Возьмите случай, когда вы хотите вставить элемент в индекс 0. Пример:
add [200, 300, 400] [] 0 100
Если вы выполните вывод для этого, вы получите:
add [200, 300, 400] [] 0 100
add [300, 400] (200:100:[]) (-1) 100
add [400] (300:[200, 100]) (-2) 300
add [] (400:[300, 200, 100]) (-3) 400
reverse [400, 300, 200, 100]
[100, 200, 300, 400]
Но мы добавляем только элемент в начало списка! Такая операция проста! Он (:)
add [200, 300, 400] [] 0 100
100:[200, 300, 400]
[100, 200, 300, 400]
Подумайте, сколько из списка действительно нужно обратить вспять.
Вы спрашиваете, изменяет ли среда выполнения указатели в связанном списке. Поскольку списки в Haskell неизменяемы, никто (даже среда выполнения) не изменяет указатели в связанном списке. Вот почему, например, дешево добавить элемент в начало списка, но дорого добавить элемент в конце списка. Когда вы добавляете элемент в начало списка, вы можете повторно использовать весь существующий список. Но когда вы добавляете элемент в конце, он должен создать совершенно новый связанный список. Неизменяемость данных необходима для того, чтобы операции в начале списка были дешевыми.
Ответ 5
Re: добавив элемент в конец списка, я бы предложил использовать оператор (++)
и splitAt
:
add xs a n = beg ++ (a : end)
where
(beg, end) = splitAt n xs
List
является связанным списком, но он доступен только для чтения. Вы не можете изменить List
на месте - вместо этого вы создаете новую структуру List
, в которой есть нужные элементы. Я не читал его, но эта книга, вероятно, попадает в ваш основной вопрос.
НТН
Ответ 6
Компилятор может выбрать любое внутреннее представление, которое он хочет для списка. И на практике это действительно меняется. Очевидно, что список "[1..]" не реализован как классическая серия cons-ячеек.
Фактически, ленивый список хранится как thunk, который оценивает ячейку cons, содержащую следующее значение и следующий thunk (thunk - это в основном указатель на функцию плюс аргументы для функции, которая заменяется фактическим значением один раз функция вызывается). С другой стороны, если анализатор строгости в компиляторе может доказать, что весь список всегда будет оцениваться, тогда компилятор просто создает весь список как серию cons-ячеек.