Что хороший путь состоит в том, чтобы реализовать изменяемые структуры данных в F#? Причина, которую я спрашиваю, состоит в том, потому что я хочу возвратиться и реализовать структуры данных, о которых я узнал в уроке алгоритмов, который я посещал в этом семестре (пропустите списки, вывихните деревья, деревья сплава, y-fast попытки, деревья van Emde Boas, и т.д.), который был чистым курсом теории без кодирования вообще, и я полагаю, что мог бы также попытаться изучить F#, в то время как я делаю его. Я знаю, что "должен" использовать деревья пальца для получения косой древовидной функциональности на функциональном языке, и что я должен сделать что-то с ленью для получения функциональности списка пропуска, и т.д., но я хочу закрепить основы, прежде чем я попытаюсь играть с чисто функциональными реализациями.
Существует много примеров того, как сделать функциональные структуры данных в F#, но нет очень о том, как сделать изменяемые структуры данных, таким образом, я запустил путем согласовывания двунаправленного связанного списка здесь во что-то, что позволяет, вставляет и удаляет где угодно. Мой план состоит в том, чтобы превратить это в список пропуска и затем использовать подобную структуру (дизъюнктное объединение записи) для древовидных структур, которые я хочу реализовать. Прежде чем я запущу на чем-то более существенном, существует ли лучший способ сделать изменяемые структуры как это в F#? Я должен просто использовать записи и не обеспокоиться дизъюнктным объединением? Я должен использовать класс вместо этого? Этот вопрос "даже неправильно"? Я должен делать изменяемые структуры в C# и не падение в F#, пока я не хочу сравнить их с их чисто функциональными дубликатами?
И, если DU записей - то, что я хочу, я, возможно, написал код ниже лучше или более идиоматически? Кажется, что существует большое дублирование здесь, но я не уверен, как избавиться от него.
module DoublyLinkedList =
type 'a ll =
| None
| Node of 'a ll_node
and 'a ll_node = {
mutable Prev: 'a ll;
Element : 'a ;
mutable Next: 'a ll;
}
let insert x l =
match l with
| None -> Node({ Prev=None; Element=x; Next=None })
| Node(node) ->
match node.Prev with
| None ->
let new_node = { Prev=None; Element=x; Next=Node(node)}
node.Prev <- Node(new_node)
Node(new_node)
| Node(prev_node) ->
let new_node = { Prev=node.Prev; Element=x; Next=Node(node)}
node.Prev <- Node(new_node)
prev_node.Next <- Node(new_node)
Node(prev_node)
let rec nth n l =
match n, l with
| _,None -> None
| _,Node(node) when n > 0 -> nth (n-1) node.Next
| _,Node(node) when n < 0 -> nth (n+1) node.Prev
| _,Node(node) -> Node(node) //hopefully only when n = 0 :-)
let rec printLinkedList head =
match head with
| None -> ()
| Node(x) ->
let prev = match x.Prev with
| None -> "-"
| Node(y) -> y.Element.ToString()
let cur = x.Element.ToString()
let next = match x.Next with
| None -> "-"
| Node(y) -> y.Element.ToString()
printfn "%s, <- %s -> %s" prev cur next
printLinkedList x.Next
Я думаю, что использование размеченного объединения вместе с изменяемой записью - хороший подход. Дискриминационные союзы необходимы для сопоставления с образцом. Альтернативой использованию изменяемой записи может быть создание случая объединения с изменяемыми ссылочными ячейками :
// Note: using ´ instead of ' to avoid StackOverflow syntax confusion
type LinkedList<´T> =
| None
| Node of (LinkedList<´T> ref) * 'T * (LinkedList<´T> ref)
Это может привести к немного более простому коду. Например, функция insert
будет выглядеть так (я не пробовал, но думаю, что это должно быть правильно):
let insert x l =
match l with
| None -> Node(ref None, x, ref None)
| Node(prev, v, next) as node ->
match !prev with
| None ->
prev := Node(ref None, x, ref node)
!prev
| Node(_, _, prevNextRef) ->
prevNextRef := Node(ref (!node.Prev), x, ref node)
prev := !prevNextRef
!prevNextRef
Однако я не думаю, что это делает код более лаконичным ( может быть, даже чуть менее читабельный). В любом случае вы можете определить активный шаблон, чтобы различать три случая в функции insert
, используя одно выражение match
. Ниже приводится исходная структура данных. Это просто экстрактор элементов, хранящихся в записи:
let (|NodeInfo|) node = (node.Prev, node.Element, node.Next)
Для получения дополнительной информации см. Активные шаблоны в MSDN . Тогда функция insert
будет выглядеть так:
let insert x l =
match l with
| None -> Node({ Prev=None; Element=x; Next=None })
| Node(NodeInfo(None, _, _) as node) ->
let new_node = { Prev=None; Element=x; Next=Node(node)}
node.Prev <- Node(new_node)
Node(new_node)
| Node(NodeInfo(Node(prev_node), _, _) as node) ->
let new_node = { Prev=node.Prev; Element=x; Next=Node(node)}
node.Prev <- Node(new_node)
prev_node.Next <- Node(new_node)
Node(prev_node)
[EDIT ] На самом деле то же самое можно было бы сделать, используя шаблоны для извлечения элементов записи, но я думаю, что активные шаблоны могут сделать код немного лучше.
Причина, по которой я спрашиваю, заключается в том, что я хочу вернуться и реализовать структуры данных, о которых я узнал в классе алгоритмов, который я посещал в этом семестре (списки пропусков, деревья сплайнов, деревья слияния, y-fast tries, деревья ван Эмде-Боаса и т.д.), который был чисто теоретическим курсом без какого-либо кодирования, и я подумал, что я мог бы также попытаться выучить F#, пока я это делаю.
Если это образовательное упражнение, то вы вполне можете обнаружить, что некоторые структуры данных (такие как списки пропусков) проще и познавательнее реализовать в их чисто функциональной форме. Окасаки является отличной ссылкой для элегантных чисто функциональных структур данных и очень познавателен (но имеет мало практической пользы).
Прежде чем я приступлю к чему-то более существенному, есть ли лучший способ сделать изменяемые структуры, подобные этой, в F#?
Вы работаете в правильном направлении. Если вам нужны примеры, посмотрите на реализацию mutable heap, написанную на OCaml Жаном-Кристофом Филлиатре. Есть много других замечательных примеров простых и эффективных структур данных с изменяемыми параметрами, использующих функциональное программирование в своих API, которые написаны на OCaml.
Должен ли я просто использовать записи и не беспокоиться о дискриминированном объединении?
В случае с изменяемыми структурами данных их части часто хранятся смежно в массиве. Именно поэтому они часто намного эффективнее своих чисто функциональных аналогов.
Должен ли я делать изменяемые структуры на C# и не погружаться в F#, пока не захочу сравнить их с чисто функциональными аналогами?
Нет. Не используйте F# так, как будто это чисто функциональный язык программирования. Весь смысл чисто функциональных языков (и причина их большей популярности) в том, что мутация полезна и ее не всегда следует избегать. Массивы и хэш-таблицы - отличные структуры данных. Мутирующие структуры данных, подобные тем, которые вы перечислили, также могут быть полезны.