6

While I was learning functional programming I have encounterd two for me very similar terms: immutability and persistence. Also I have read simular questions from stackoverflow, but I am still confused.

Wikipedia says:

In object-oriented and functional programming, an immutable object (unchangeable object) is an object whose state cannot be modified after it is created.

In computing, a persistent data structure or not ephemeral data structure is a data structure that always preserves the previous version of itself when it is modified.

So the only way to change the immutable variable state is to assign a new value to its identifier, the old value will be deleted if there are no other variables pointing to itself. Every other variable pointing to old state will not see changes (keeping old state). Persistent data structures behaves the same, also both persisten and immutable objects are used in concurrent programming. I am confused, are there any differences between those two terms? My current understandings are that every persistent data structure is immutable and vice versa.

7
  • 3
    I don't see how these two terms are related in any way, shape, or form. Can you clarify how you believe these terms are related? Commented Jun 26, 2023 at 18:59
  • 1
    What Greg says. In particular, your second definition ends with "when it is modified"; that is something which does not happen to immutable objects. Commented Jun 26, 2023 at 19:22
  • 3
    Note that the tag persistence you used on your question relates to a completely different, distinct concept than the one you are talking about. This seems to have caused some confusion. I have taken the liberty of removing the tag from your question and creating a new one persistent-data-structures Commented Jun 27, 2023 at 4:30
  • 1
    @GregBurghardt Persistent means "remains across different invocations of a program", while immutable means "remains unchanged during the same invocation of a program". Still different, but it's not unreasonable to consider these two related terms. Commented Jun 27, 2023 at 6:18
  • 2
    I can see the OP's (and my own) confusion here. "Persistence" and "Persistent Data Structures" are similar terms, but have different meanings. I can see how they are related, and I can see how someone might think they aren't related. Good question. Commented Jun 27, 2023 at 15:33

2 Answers 2

7

tl;dr summary: every persistent data structure is immutable, i.e. persistence is a subset of immutability, but not the other way around.

Note: the Wikipedia article is about the more general concept of persistent data structures, not limited to functional programming. You are asking specifically about the context of functional programming, though, where the term is used in a narrower sense, so I will restrict my answer to this narrower sense.

Immutability

Immutable means unchangeable, i.e. something which cannot be changed. I've never learned Latin, but I am reasonably sure that is quite literally the translation with im- being a negative prefix – think of immature, immaterial, etc. – and mutability meaning changeability – think of mutation.

The primary example of a data type that is immutable even in languages that otherwise fully embrace mutability is numbers. I can't name any language off the top of my head that would allow to mutate the state of a number such that, e.g. 1 + 1 suddenly equals 4.

An example of a more complex data structure that is immutable in some (but not all) languages, is strings. They are immutable in Java, C#, Python, and ECMAScript, all of which are languages which otherwise embrace mutability. They are mutable in Ruby, for example, although efforts are underway to restrict that mutability.

Immutability means that I cannot just "change" a sub-element of a complex data structure, such as an array. If I have an immutable array [1, 2, 3], I cannot change an element of the array so that I now have [1, 99, 3]. I have to copy the entire array with one element changed, so that I now have two arrays, I still have the original [1, 2, 3] and I also have the new [1, 99, 3].

This copying process is slow: every time I want to change one element, I need to copy the entire array, i.e. changing one element has a worst-case step complexity of θ(length of the array).

This is where Persistence comes in:

Persistence

An informal definition of a persistent data structure is a data structure that is BOTH

  • immutable AND
  • has "efficient operations".

Now, I don't want to go into too much detail of what we mean by "efficient operations", but I want to motivate it with an example. Let's look at dynamic arrays.

Example: dynamic arrays

Dynamic arrays are arrays which can change their size, i.e. you can add and remove elements. Many collections libraries have dynamic arrays, both mutable (e.g. Python's list, Ruby's Array, Java's ArrayList, .NET's ArrayList) and immutable (e.g. Clojure's Vector, Scala's Vector).

Mutable dynamic arrays

… naïve implementation

A simple and naïve implementation of mutable dynamic arrays is to allocate a new array of size n+1 and copy all the existing elements over every time you append an element. This gives you a worst-case step complexity of θ(1) for indexing and mutation of an element, just like a mutable non-resizable array, and it gives you θ(length of the array) appending, which is an operation a static array cannot do.

… the clever implementation

However, we can do better by using geometric resizing. Instead of just adding one storage place when we append one element, we add c × n storage places (i.e. a constant multiplier of the current size) whenever we run out of storage. A popular textbook choice is c = 2, i.e. doubling the array in size whenever it gets full, although smaller constants are sometimes preferable.

This still has the same worst-case step complexity as we had before, since we still need to copy the entire array in the worst case.

However, the amortized worst-case step complexity is much better: it is θ(1) for appending. Dynamic arrays are the standard introductory example for amortized analysis, so I will not repeat the proof here as you can find it readily online or in any textbook. Just to give you an intuition, though, the fundamental "trick" is that the "worst-case" of needing to copy all θ(n) elements only happens every θ(n) operations, so if you amortize this worst-case "cost" over many successive appends, each append on average only takes θ(1).

Immutable dynamic arrays

… naïve implementation

The naïve implementation of immutable dynamic arrays is that we copy the array for every "mutating" operation. This means that almost all operations, i.e. append, pop (delete last element), insert in the middle, delete from the middle, and mutate (change an element at a fixed index) are θ(n), only indexing is θ(1).

This is similar to the naïve mutable dynamic array, except that the naïve mutable array has θ(1) mutation of an element.

So, this is clearly an undesirable implementation. Can we get an implementation that is closer to the performance of the "clever" mutable dynamic array implementation but still immutable?

… implementation based on difference

One idea for implementing immutable data structures efficiently is to only record the difference to the previous version instead of copying the whole thing. So, for example, when inserting an element into the array, instead of copying the whole array, we create a special "array proxy" that behaves externally just like an array, but internally, it contains a reference to the previous array and a description of the difference, e.g. "at index i, insert value 42, shift all later indices by 1".

However, this means that the operations get slower the longer you use the data structure, as the list of differences gets longer and longer and ever more complex. Everybody who has ever used a version control system that is based on storing a forward chain of patches knows that checking out a file takes longer and longer the older the project is, because in order to reconstruct the current state of the file, all patches have to be applied one-by-one from the original version.

We can implement a scheme similar to the clever dynamic mutable array, where we "collapse" the differences into a new array when they reach a certain size or complexity. However, that still means that we have different performance characteristics for what looks like the same array, depending on how it was constructed. For example, if I construct a new array directly as [1, 2, 3, 4, 5], it will have faster operations than if I start with an empty array [], then append 1, then append 2, and so on, even though the result should behave the same.

This is what persistent data structures are about: they are immutable and they are efficient, and this efficiency applies regardless of how many "version changes" there have been.

The solution to our problem is structural sharing.

… efficient implementation exploiting structural sharing

Aside: CONS lists

The simplest example of a data structure exploiting structural sharing is the CONS list. The CONS list is built like this:

  • We have special object which is the empty list (often called () or NIL).
  • A list is either the empty list or a pair of references <head, tail> where head points to the first element of the list and tail points to the rest of the list.

Here is a (runnable) example in Scala:

sealed trait MyList[+A]:
  @throws(classOf[NotImplementedError]) def head: A
  def tail: MyList[A]
  override def toString: String

case object MyNil extends MyList[Nothing]:
  @throws(classOf[NotImplementedError]) override def head = ???
  override val tail = this
  override val toString = "()"

case class MyCons[+A](val head: A, override val tail: MyList[A]) extends MyList[A]:
  override val toString = s"($head, $tail)"

val list1 = MyCons(4, MyCons(5, MyNil))
val list2 = MyCons(1, MyCons(2, MyCons(3, list1)))

val second = list2.tail.head

list1.toString //=>             (4, (5, ()))
list2.toString //=> (1, (2, (3, (4, (5, ())))))
second         //=> 2

As you can see, we were able to efficiently construct a new list (list2) based on an existing list (list1), by the fact that list1 and list2 share their structure. (In fact, in this simple example, list1 is fully contained within list2.) Even though the two lists share their structure, everybody who has a reference to the head of list1 can still independently use it without even having to know about list2 and vice versa.

This gives us θ(1) prepending of a new element at the head. If we want to insert, delete, or "mutate" an element in the middle, we only need to copy the path from the head up to the mutation point, but we can still share the tail with the previous version.

This kind of structural sharing is used by many widely-used persistent data structure implementations.

Generalizing lists to trees

In particular, many persistent data structure implementations are based on trees. I started with the list first, because I found it easier to understand how structural sharing and path copying works in lists and then generalize this understanding to trees. After all, you can think of a list as a degenerate tree where each node has only a single child. Conversely, once you understand how CONS lists work, how they share their structure, and how path copying works, you can sort-of imagine a persistent tree as a CONS list with more than one head value and more than one tail.

And that is exactly how typical persistent dynamic arrays are implemented: as trees.

You probably have learned about trees already. The vast majority of trees that are taught are binary trees where each node has two children. And you can build a dynamic array this way. You can think of a dynamic array as a binary search tree where the array index is the search key.

Just like with the CONS list example, a newer version and an older version can share a sub-structure. In the case of the CONS list, that was a list tail, in the case of a tree, that will be a sub-tree. And just like with the CONS list, when you make a change in the middle of the tree, you only need to copy the path from the root to the location of the change, but not the entire tree. The rest of the tree structure can still be shared!

In reality, persistent dynamic arrays don't use binary trees, they use much wider, much shallower trees where the tree nodes are themselves immutable arrays. This improves cache locality, i.e. multiple consecutive elements of the "tree array" are actually implemented as multiple consecutive elements of the internal immutable array (which is an actual array, i.e. a contiguous region of memory). It also reduces the number of "steps" you need to take until you find the value.

For example, Clojure's Vector uses a 32-ary tree, i.e. each node is an array of size 32. That means it takes at most log32(n) steps to go from the root to the leaf of an array with n values. While this is still, technically θ(log n) for indexing, in reality, log32 grows very slow and is almost indistinguishable from O(1) even for billions of elements.

This is an example where constants do matter! Rich Hickey, the creator of Clojure, likes to say that Clojure's Vectors are "θ(log32 n)", where the 32 is not irrelevant. Just as an example: Arrays and mutable ArrayLists in Java can only have a maximum of 2,147,483,647 elements. log32(2147483647) is slightly less than 6.2, meaning it will take a maximum of 7 pointer dereferences plus one array access to find a value in a Clojure Vector as opposed to one pointer dereference and one array access in a Java ArrayList.

Which means that not only is our dynamic array immutable and persistent with preservation of old versions and efficient operations regardless of number of versions, but it is even almost as efficient as its mutable counterpart.

Conclusion

So, in conclusion, the difference between an immutable data structure and a persistent data structure in functional programming is that persistent data structures are immutable with additional restrictions, namely:

  • Efficient operations AND
  • Preservation of old versions

Where efficient operations applies both to new and old versions.

Often, they are also "efficient" compared to their mutable counterparts.

Further reading

As mentioned in Karl Bielefeldt's answer, Prof. Chris Okasaki, Ph.D.'s book Purely Functional Data Structures, based on his Ph.D. thesis of the same name is the definitive work on immutable and persistent data structures.

However, there have been some improvements since then. Phil Bagwell created the Hash Array Mapped Trie (HAMT). Rich Hickey used HAMTs in Clojure with some tweaks. Phil Bagwell then improved on those tweaks when he helped re-write the collections library in Scala 2.8.

There is a big list question on the sister site cstheory.se titled What's new in purely functional data structures since Okasaki? which has links to a lot of interesting stuff.

Functional programming

Please note that everything I wrote above only applies within the context of .

In the Wikipedia article, a distinction is made between fully persistent and partially persistent:

In the partial persistence model, a programmer may query any previous version of a data structure, but may only update the latest version. This implies a linear ordering among each version of the data structure. In the fully persistent model, both updates and queries are allowed on any version of the data structure.

In functional programming, we only care about the fully persistent model. The distinction between versions that can be updated and ones which can't doesn't make sense for the way we use persistent data structures in FP. In FP, we use persistent data structures as immutable data structures with efficient operations. We don't consider them to be "versions", they are all just values that we want to manipulate.

The Wikipedia article continues:

In some cases the performance characteristics of querying or updating older versions of a data structure may be allowed to degrade […]

Again, when we talk about persistent data structures in the context of functional programming, we do not allow this.

Regarding this statement in your question:

My current understandings are that every persistent data structure is immutable and vice versa.

Neither of those two things is true in general.

Only within the context of functional programming is the first statement true: every persistent data structure is an immutable data structure. (Although that is trivially true in functional programming, as everything is immutable.)

In functional programming, a persistent data structure is a special kind of immutable data structure with efficient operations.

The general concept of persistent data structures, though, is broader then immutable data structures.

Red herring: persistent storage

To add to the confusion, the term persistence can have another, very different, meaning.

The term persistence is also used in the context of persistent storage. Here, it means data storage which outlives the lifetime of the process which created the data.

A typical example of persistent storage would be a hard disk. The antonym here would be something like "volatile".

While there is some relationship in the etymology of those two terms, the concepts are completely different and from distinct sub-fields.

Note that the tag you used on your question refers to this kind of persistence, not the kind, which seems to have caused some confusion among answerers and commenters.

2
  • Thank you! I finally fully understand the difference between those two terms. Also, thanks for the guidelines for further study :) Commented Jun 27, 2023 at 17:26
  • Mutable numbers: In Fortran IV constants passed as parameters could be mutated. So I could change 2 to 4, and the result of adding 1+1 would be 4 :-( It was a horrible problem in the language definition, not a compiler bug.
    – gnasher729
    Commented Jun 28, 2023 at 15:54
6

Persistence means something very different to most imperative programmers, so there might be some confusion if you're searching the term.

The simplest persistent data structure in the functional/immutable sense is a linked list. Say you have the following linked list:

2 -> 3 -> 4

And you prepend a 1 to the list, giving:

1 -> 2 -> 3 -> 4

Note that any pointer that pointed to the previous version of the list with head 2 still works just fine. Most of the data structure was reused, not copied. Persistent data structures are a subset of immutable data structures that have this sharing property. Without persistence, you basically have to copy the entire list whenever you make a change if you want to maintain immutability.

You can extend that same sharing idea to trees and then use trees to make even more complex structures. Chris Okasaki wrote the canonical book on the subject if you want to learn more.

2
  • 3
    Thank you for being the only one to actually answer the question! Commented Jun 26, 2023 at 22:55
  • Thank you! I read about the most primitive technique copy-on-write for modifying variable state, which copies the whole structure without sharing structure parts. I am confused about that detail. geeksforgeeks.org/persistent-data-structures Commented Jun 27, 2023 at 8:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.