Introduction
An immutable (or
persistent) data structure is a data structure that can not be modified.
- So... is esc>ReadOnlyCollection
an inmutable data structure?
Not really, at least not of the same type. Immutable data structures do have primitives that look like they are going to change state, like Add, Remove, etc. But what they do instead is yielding a new, fresh, data structure with the modifications.
- So... that should be really expensive in memory consumption!
Not that much, they are cleverly designed to reuse the previous version of the structure, or parts of it, and since they are not going to be modified, it's completely safe to do so.
- Mmm... clever thing, but is there something actually... useful you can do with this stuff?
Concurrency! You can make lock-free global caches that can be used by many threads at the same time in an optimistic fashion. See more about that in Sync.
Currently we have three Immutable Data Structures: ImmutableStack, a ImmutableQueue, and ImmutableAVLTree.
Where the structures comes from (Advanced)
Our implementation is based on the classes Stack, Queue, and AVLTree defined in Eric Lippert's Blog series of post. If you want to learn a lot about immutability:
Part One: Kinds of Immutability
Part Two: A Simple Immutable Stack
Part Three: A Covariant Immutable Stack
Part Four: An Immutable Queue
Part Six: A Simple Binary Tree
Part Seven: More on Binary Trees
Part Eight: Even More On Binary Trees
Part Nine: Academic? Plus my AVL tree implementation
Part 10: A double-ended queue
Part Eleven: A working double-ended queue
We made some changes, though:
- Remove Interfaces: We remove all the interfaces (IStack, IQueue, IBinaryTree, IMap and IBinarySearchTree) in order to reduce overall complexity, and more importantly, to make client code simpler:
IStack<int> stack = Stack<int>.Empty;
Stack<int> stack = Stack<int>.Empty;
In order to do that, we make EmptyStack, EmptyQueue and EmptyAVLTree inherit from Stack, Queue, and AVLTree directly, making the methods virtual. They still sealed 'in spirit' because the constructor is private.
- Flip Real/Empty classes: Due to the previous change, the Empty version of the data structures inherit some fields that they really don't need, so what we did then was, instead of having:
public class Stack<T>
{
class EmptyStack : Stack<T>
{
}
}
Move the Empty version to be the public interface, and the Full version the private nested class instead.
public class Stack<T>
{
class FullStack : Stack<T>
{
}
}
- Small changes in usability: Like adding ToString to the collections, or some improvements in AVLTrees (indexer, TryGetValue, and enumerating in order).
- Change class names: In order to make them actually usable we had to add Immutable prefix before every collection. Otherwise they conflict with System.Collection.Generic classes.
ImmutableStack
The most simple immutable data structure, can be used as a stack or as a list, if you are not planning to insert elements in the middle (or the end).
Internally is just an immutable linked list of nodes, where each node has one value and a link to the next. So Push, Pop and Peak are all O(1).
public class ImmutableStack<T>: IEnumerable<T>
{
public static ImmutableStack<T> Empty { get; }
public bool IsEmpty { get; }
public ImmutableStack<T> Push(T value)
public ImmutableStack<T> Pop()
public T Peek()
public virtual IEnumerator<T> GetEnumerator()
public override string ToString()
}
public static class ImmutableStackExtensions
{
public static ImmutableStack<T> Reverse<T>(this ImmutableStack<T> stack);
public static ImmutableStack<T> Reverse<T>(this ImmutableStack<T> stack, ImmutableStack<T> initial);
}
Also, since ImmutableStack implements IEnumerable, all the Linq extension methods are applicable.
Example:
ImmutableStack<int> stack = ImmutableStack<int>.Empty;
for (int i = 0; i < 10; i++)
stack = stack.Push(i);
Console.WriteLine(stack.ToString());
while (!stack.IsEmpty)
{
Console.Write(stack.Peek());
stack = stack.Pop();
}
Console.WriteLine(stack.ToString());
ImmutableQueue
Queue allows the introduction of items from one side of the list (Enqueue) and retrieves them from the other part (Dequeue). As usual, all the operations retrieve a new instance.
Internally it uses a two ImmutableStacks as buffers, one for queuing and other for dequeuing. When the one for dequeuing becomes empty, the one for queuing is reversed and used for dequeuing. So every operation has an amortized cost of O(1) but some dequeuing can get O(n) sometimes.
public class ImmutableQueue<T> : IEnumerable<T>
{
public static ImmutableQueue<T> Empty {get;}
public bool IsEmpty { get; }
public T Peek() {get;}
public ImmutableQueue<T> Enqueue(T value)
public ImmutableQueue<T> Dequeue()
public IEnumerator<T> GetEnumerator()
public override string ToString()
}
Example:
ImmutableQueue<int> queue = ImmutableQueue<int>.Empty;
for (int i = 0; i < 10; i++)
queue = queue.Enqueue(i);
Console.WriteLine(queue.ToString());
while (!queue.IsEmpty)
{
Console.WriteLine(queue.Peek());
queue = queue.Dequeue();
}
Console.WriteLine(queue.ToString());
ImmutableAVLTree
An AVL tree is a binary tree used for implementing sorted dictionaries.
Internally each node has a key-value pair and a left and right child. The think that makes AVL Tree 'special' is his ability to self-balance to keep the tree as sort as possible, keeping the cost of the operations O(log N).
public class ImmutableAVLTree<K, V>
where K : IComparable<K>
{
public static ImmutableAVLTree<K, V> Empty {get;}
public bool IsEmpty { get; }
public ImmutableAVLTree<K, V> Add(K key, V value)
public ImmutableAVLTree<K, V> Remove(K key)
public V this[K key]{get; }
public bool TryGetValue(K key, out V value)
public ImmutableAVLTree<K, V> Search(K key)
public bool Contains(K key)
public IEnumerable<K> Keys { get; }
public IEnumerable<V> Values { get; }
public IEnumerable<KeyValuePair<K, V>> Pairs { get; }
public K Key { get; }
public V Value { get; }
public int Height { get; }
public ImmutableAVLTree<K, V> Left { get; }
public ImmutableAVLTree<K, V> Right { get; }
public override string ToString()
}
Example:
ImmutableAVLTree<int, string> tree = ImmutableAVLTree<int, string>.Empty;
tree = tree.Add(171, "Bengali");
tree = tree.Add(206, "Arabic");
tree = tree.Add(322, "Spanish");
tree = tree.Add(366, "Hindustani");
tree = tree.Add(177, "Portuguese");
tree = tree.Add(873, "Mandarin");
tree = tree.Add(309, "English");
tree = tree.Add(145, "Russian");
tree = tree.Add(122, "Japanese");
Console.WriteLine(tree.ToString());
string name;
Console.WriteLine(tree.TryGetValue(1000, out name));
Console.WriteLine(tree.TryGetValue(366, out name));
Console.WriteLine(tree[1000]);
Console.WriteLine(tree[366]);
tree = tree.Remove(1000);
tree = tree.Remove(366);
while (!tree.IsEmpty)
{
Console.WriteLine("Key {0} Value {1}", tree.Key, tree.Value);
tree = tree.Remove(tree.Key);
}
Console.WriteLine(tree.ToString());