Copy by Snapshot - Spicery/Nutmeg GitHub Wiki

Summary

A snapshot-copy returns an equal version of an object that has no mutable store. Deep snapshotting will return recursively immutable values. Shallow snapshotting will return sealed (val) values, which may opportunistically also be recursively immutable (const).

Nutmeg's copying routines have a variety of copying policies, which can be specified by the optional parameter policy. Snapshotting is the default.

y := deepCopy( x )      ### y is recursively immutable. The default policy used is #Snapshot.
z := shallowCopy( x )   ### z is sealed but may point to mutable objects.

Available policies are:

  • #Snapshot - returns a frozen-in-time view that cannot be altered
  • #Clone - faithful duplication of mutability of original
  • #LiveView - a read-only view that tracks the original

Background

In imperative programming languages such as C/C++, copying is important and ubiquitous. Copying objects is essential to avoid accidentally sharing updates. Imperative languages typically pre-emptively copy, although a few (e.g. PHP) copy on demand. The issue is that copying is quite an expensive operation, taking time proportional to the (recursively expanded) object, allocates store that has to be managed and subsequently deallocated. So copying costs both time and space, a double whammy.

Functional programming languages do not have updates and hence sharing does not carry any significant risks for the application programmer. Heavy use of sharing avoids the direct overhead of pre-emptive copying. Of course, the lack of updates has the practical downside that the programmer has to create an entire new object to represent an update, which incurs extra allocation/management/deallocation costs. The compiler may be able to introduce updates behind the scenes as an optimisation but store churn is a serious drawback - hence the focus in the functional programming community on smart garbage collection technology.

The key ancestor of Nutmeg is LISP, which can reasonably be called the first hybrid functional/imperative programming language. It made a compromise that proved workable: all mutable values were implemented by small, copyable references to mutable-objects. This avoided the overwhelming complexity of aliased variables but accepted the somewhat lesser burden of managing object-sharing. This burden is further softened by making many simple objects (integers, characters, strings, symbols) immutable.

Snapshotting

Nutmeg is a true functional/imperative hybrid that gives the programmer close-control over the blend of features required. Conceptually, Nutmeg encourages a LISP-like view that all values are objects and allocated 'in the heap' and that all variables are simply pointers to those objects. When dealing with mutable objects the programmer is therefore obliged to deal with accidental sharing by copying.

However, unlike LISP, Nutmeg has a very rich range of immutable types and even mutable objects can be sealed. We wish to avoid copying these objects and so Nutmeg uses a version of copying that, by default, simply returns these values without copying. This is integrated into snapshotting and is used pervasively in Nutmeg.

Part Ownership

A deep aspect of Nutmeg is that it is explicit about part-of relationships.

Typically, complex objects are composites involving objects of other types. When designing these complex objects one question arises again and again: is it safe to directly access or return these inner objects? The answer is that it depends on whether or not the inner object is meaningfully independent or whether it is best understood as part-of the main object. If the inner object is part-of the main object then it should never be exposed to outside scrutiny (except possibly for unit tests).

Sometimes it makes sense to return some kind of read-only snapshot of an inner object. For example, if you were modelling (say) a wallet that contained notes of different denominations, you might well model it as a mutable-list of notes (immutable integers) but additionally keep track of the total. You would never want another part of your program to get hold of your inner list - even though there are utility routines that you might want to pass it to. And you might want to mass update the contents of the wallet from another list of integers.

Snapshotting provides a good answer to this, with inner objects simply being copied-if-mutable. When objects are much bigger, snapshotting may elect to return read-only views of the inner objects (and isolate the snapshot via copy-on-demand).

Also it sometimes makes sense to return a read-only, live view of an inner object. This can be achieved with a different option to the copy functions:

y := deepCopy( x, policy=#LiveView )    ### Policy can be #Snapshot, #LiveView, #Clone. y or objects accessed from y cannot be altered but reflect change
z := deepCopy( x, policy=#LiveView )    ### z cannot be altered but will reflect changes - objects accessed from z share with the original