The way into reference semantics in the Swift world is to define a class. Here is a very simple temperature class.
class Temperature {
var celsius: Double = 0
var fahrenheit: Double {
get { return celsius * 9 / 5 + 32 }
set { celsius = (newValue - 32) * 5 / 9 }
}
}
We are storing our temperature value in Celsius. We want to have this nice computed Fahrenheit property so we can always get to it with the correct unit. Simple, abstracted version of a temperature. Lets try to use it in some simple code:
let home = House()
let temp = Temperature()
temp.fahrenheit = 75
home.thermostat.temperature = temp
We create a home instance, we create a temperature instance. We set our thermostat to a balmy 75 degrees Fahrenheit. Now we decide, its getting close to dinnertime and we want to bake some salmon. So I set my oven to 425 degrees and hit Bake.
temp.fahrenheit = 425
home.oven.temperature = temp
home.oven.bake()
We ended up heating the house, not the oven. We hit this case of unintended sharing. Think of the object graph. We have our house. It has a thermostat and an oven. We have this temperature object that temp points to.
flowchart TB
id3[temp var]
subgraph HOUSE
subgraph thermostat
id1[temperature]
end
subgraph oven
id2[temperature]
end
end
When we set out thermostat, we wired it to the same object as the temp.
flowchart TB
id3[temp var]
id1-->id3
subgraph HOUSE
subgraph thermostat
id1[temperature]
end
subgraph oven
id2[temperature]
end
end
Things look file until we go ahead and do a mutation. temp.fahrenheit = 425
flowchart TB
id3[temp var]
id1-->id3
id2-->id3
subgraph HOUSE
subgraph thermostat
id1[temperature]
end
subgraph oven
id2[temperature]
end
end
And now this unexpected sharing just caused us to set out thermostat to 425. At this point its game over, but just for good measure lets wire out thermostat to our oven.
home.oven.temperature = temp
home.oven.bake()
So what did we do wrong? In this world where you have reference semantics, you want to prevent sharing, and so you do copies. Let’s do this the right way.
let home = House()
let temp = Temperature()
temp.fahrenheit = 75
home.thermostat.temperature = temp.copy()
When I set the temperature of my thermostat, I wil cause a copy. So I get a brand new object. That’s what my thermostat’s temperature points to. When I go ahead and change the temperature of my temp variable, it does not affect it.
temp.fahrenheit = 425
Now I go and set the oven’s temperature, I wil make another copy.
home.oven.temperature = temp.copy()
home.oven.bake()
Technically, I don’t need this last copy. It’s inefficient to fo waste time allocating memory on the heap to create this extra copy. But Im going to be safe because the last time I missed a copy, I got burned.
class Oven {
var _temperature: Templerature = Temperature(celsius: 0)
var temperature: Temperature {
get { return _temperature }
set { _temperature = newValue.copy() }
}
}
We refer to this as defensive copying, where we copy not because we know we need it, but because if we do happen to need it down the road, its really hard to debug these problems. And so it’s way too easy to forget a dot copy any time we assigned a temperature somewhere in our oven. So instead, I will bake this behavior right right into my oven. Naturally I have to do this exact same thing for the thermostat.
So now we’ve got a whole bunch of boilerplate we are copying and pasting, and sooner or later, you will be knocking on my door asking for a language feature here. Let’s hold off on that and talk about where we see copying in Cocoa and Objective-C.
Cocoa requires copying throughout:
- NSCopying codifies copying an object
- NSString, NSArray, NSDictionary, NSURLRequest, etc. all require copying
So in Cocoa, you have this notion of NSCopying protocol that codifies what it means to copy, and you have a whole lot of data types like NSString, NSArray etc. These things conform to NSCopying because you have to copy them to be safe. We are in a system that needs copying, and so you see a lot of defensive copying for a very, very good reasons.
- NSDictionary calls -copy on its keys. Why? If you were to hand NSDictionary a key to insert it and then go change it in a way that, for example, alters the hash value of it, you will break all the internal variance of NSDictionary and blame us for your bugs.
- Property attribute “copy” provides defensive copying on assignment
What we really do is we do defensive copying in NSDictionary. That’s the right answer in this system, but it’s unfortunate that we are losing performance b/c we are doing these extra copies. Of course, we moved this all the may down into the level of the language with copy properties in Objective-C that do defensive copying on every single assignment to try to prevent these problems, and it helps, all this defensive copying helps, but it’s never good enough as bugs appear due to missed copies.
Is Immutability the Answer?
- Functional programming languages have reference semantics with immutability
- Eliminates many problems caused by ref. semantics with mutation
- No worries about unintended side effects
So we have problems with those reference semantics and there’s mutation. Maybe the problem here is not the reference semantics but the mutation itself. Perhaps we should move to a world of immutable data structures with reference semantics. If you go talk to someone in the functional programming community, they will say, yeah, we have been doing this for decades. And it does improve things there. So you can’t have unintended side effects in a world where there are no side effects and so immutable reference semantics are a consistent way to work in the system. It does not have these unintended consequences, which we ran into in our little temperature example.
The problem is that immutability has some disadvantages.
Let’s talk through a couple of these. We will take this temperature class of ours and try to make it safer by making it immutable. So, all we had to do here was change the stored property Celsius from a var to a let, and now you can’t change it.
class Temperature {
let celsius: Double = 0
var fahrenheit: Double { return celsius * 9 / 5 + 32 }
init(celsius: Double) { self.celsius = celsius }
init(fahrenheit: Double) { self.celsius = (fahrenheit - 32) * 5 / 9 }
}
And then the Fahrenheit computed property loses its setter. So you can not change the state of temperature no matter what you do. We add some initializers for convenience. Okay, let’s talk about awkward interfaces. Before, if I wanted to, say, tweak the temperature of my oven by 10 degrees Fahrenheit, this was a simple operation.
home.oven.temperature.fahrenheit += 10.0
After you’ve taken away mutation we will have to go and grab the temperature object in the oven, create yet another temperature object that has the new value.
let temp = home.oven.temperature
home.oven.temperature = Temperature(fahrenheit: temp.fahrenheit + 10.0)
There is more code, and we wasted time allocating another object on the heap. But in truth, we have ot embraced immutability here b/c we did an assignment that mutated the oven itself. If we were embracing the notion of immutable reference types throufhout here, we would be creating a new temperature to put in a new oven to put in a new home.
Let’s get a little bit more theoretical and do some math. So the Sieve of Eratosthenes is an ancient algorithm for computing prime numbers. It uses mutation and actually lends itself well to drawing things out in the dirst with a stick. So this is the implementation of the mutating version in Swift.
func primes(n: Int) -> [Int] {
var numbers = [Int](2..<n)
for i in 0..<n-2 {
guard let prime = numbers[i] where prime > 0 else { continue }
for multiple in stride(from: 2 * prime - 2, to: n-2, by: prime) {
numbers[multiple] = 0
}
}
return numbers.filter { $0 > 0 }
}
We are going to walk through it so you get the idea behind it.
Create an array. Notice the var b/c we are going to mutate this array. Notice this array is from 2, first prime number, up through whatever number you want to compute.
flowchart
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Now the outer loop, each time through it, we will pick the next number in the array. That number is a prime number, P.
What the inner loop is going to do is walk over all of the multiples of P, erasing them from the array by setting them to zero. Because, of course, if you are a multiple of a prime number, you are not prime.
flowchart
a[2]
b[3]
c[0]:::hlight
a1[5]
b1[0]:::hlight
c1[7]
a2[0]:::hlight
b2[9]
c2[0]:::hlight
a3[11]
b3[0]:::hlight
a4[13]
b4[0]:::hlight
c4[15]
a5[0]:::hlight
b5[17]
c5[0]:::hlight
a6[19]
b6[0]:::hlight
classDef hlight fill:#f96
Back to the outer loop, we go and grab the next number, it’s a prime number. We erase all of its multiples from the array. So it’s a very, very simple algorithm. Think of the stick in the dirt. You are just scratching things out.
2|3|0|5|0|7|0|0|0|11|0|13|0|0|0|17|0|19|0| –=—————————————
Once we are done going through all of our interactions we go down to “return”. And we do the last simple operation, which says, everything that we have not zeroed out in the array, that’s part of our result. So we will do that with filter.
2|3|5|7|11|13|17|19| ——————–
Simple algorithm, entirely based on mutation. Now, that does not mean you can not express it in a world without mutation.
Haskell ——-
In a pure functional language like Haskell, it looks like this:
| primes = sieve [2..] | |
| sieve [] = [] | |
| sieve (p : xs) = p : sieve [x | x <- xs, x ‘mod’ p > 0] |
This is the Haskell formulation. It’s beutiful, it’s functional, it does not mutate at all.
Swift —– Here’s a very similar implementation b/c it turns out Swift can do functional also, and if you want to make it lazy, that’s an exercise for the reader but it’s not thats much harder.
func sieve(numbers: [Int]) -> [Int] { if numbers.isEmpty { return [] } let p = numbers[0] return [p] + sieve(numbers[1..<numbers.count].filter { $0 % p > 0 } }
We are going to walk through how this algorithm works, b/c its very, very similar.
2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20| ————————————————
In the simple basis case, if there is no nimbers, there is no prime numbers in there so we return an empty array.
2 |3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|
2 |3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|
3 |5|7|11|13|17|19|
5 7|11|13|17|19|
…
What happens here is you end up building along this left-hand diagonal the actual prime numbers, and as a result they all get concatenated together. The idea is similar, very similar. But it’s not the same algo b/c it has different performance characteristics. So this result comes from a brillant paper by Melissa O`Neil called “The Genuine Sieve of Eratosthenes” where she showed the Haskell commumity that their beloved sieve was not the real sieve b/c it did not perform the same as the real sieve. She goes through much more complicated implementations in Haskell, that can get back to the performance characteristics. Read the paper and check it out. Here’s a taste:
Look at either the Haskell list comprehention
| [x | x <- xs, x mod p > 0] |
or the equivalent Swift filter
.filter { $0 % p > 0 }
In this nonmutating version, this operation will walk over every single element in the array and do a division operation to see if it should still be in the next step, to see if it’s a multiple of P or not. In the original mutating algo, we only walked over the multiples of the prime number and those, of course, become increasingly sparse as you get to bigger and bigger numbers. So you are visiting fewer elements, and moreover, you only have to do an addition to get to the next element. So you are doing less work per element. And the non-mutating version is not as efficient as the mutating version without a whole ton of work.
Let’s bring it back to Cocoa. Cocoa has a number of immutable classes
So you don’t have to worry about your sharing having unintended side effects. But you also see the downsides there when you work with it, like:
[url URLByAppendingPathComponent: component];
For example this is how I would create an URL by starting with home directory and adding successive path components to get to some directory
NSURL *url = [NSURL alloc] initWithString: NSHomeDirectory()]; NSString *component; while ((component = getNextSubdir()) { url = [url URLByAppendingPathComponent: component]; }
And I wanted to do it without the mutation in the reference semantics world. So I created an NSURL. Each time through the loop, I create a new URL by appending the next path component. This is not a great algo. Every time through, I’m creating an NSURL, another object, the old one goes away, and then the NSURL is going to copy all of the string dat each time through the loop. Not very efficient. Are we doing it wrong, should we really be collecting all these components into an NSArray and then use fileURLWithPathComponents ?
Fine. But, remember, we are embracing immutability here. So when I create my array, I will create an NSArray with a particular object, that’s the home directory. Each time though, I create a new array, adding one more object.
NSArray <NSString >array = [NSArray arrayWithObject: NSHomeDirectory()]; NSString *component; while ((component = getNextSubdir()) { array = [array arrayByAddingObject: component]; } url = [NSURL fileURLWithPathComponents: array];
Im still copying the elements. Im still quadratic (O(n²)) in terms of performance. Im not copying string data so it’s a little better. This is why we don’t fully embrace immutability in the world of Cocoa b/c it does not make sense. Instead you use mutability in more localized places where it makes sense.
NSMutableArray<NSString *> *array = [NSMutableArray array]; [array addObject: NSHomeDirectory()]; NSString *component; while ((component = getNextSubdir()) { [array addObject: component]; } url = [NSURL fileURLWithPathComponents: array];
Immutability is a good thing. It makes the refence semantics world easier to reason about. But you can not go completely to immutability or you start to go crazy. That brings us to Value Semantics.
Value semantics —————
With value semantics, we take a different approach. We like mutation. We think it’s valuable. We think it’s easy to use when doen correctly. The problem, as we see it, is in sharing.
Integers are value types Mutating one variable of some value type will never affect a different variable
How it works?
The idea is simple: if you have two variables, the values in those variables are logically distinct.
var a: Int = 17 var b = a assert(a == b) b += 25 print(“a =(a), b = (b)”) // a = 17, b = 42
So I have an integer A, I copy it over to an integer B. Of course they are equivalent, its a copy.
I go to mutate B. If I told you that would change A, you would say Im crazy. They have value semantics in every language we have ever worked with.
CGPoint for example. Copy from A to B, mutate B, it’s not going to have any effect on A. You are used to this. If CGPoint did not behave this way, you would be really, really surprised.
The idea of value semantics is to take this thing we already know and understand for the very fundamental types, like numbers, and small structs containing numbers, and extend it outward to work with much, much richer types.
var a: String = “Hello” var b = a assert(a == b) b.extend(“ WWDC!”) print(“a = (a), b = (b)”) // a = Hello, b = Hello WWDC!
So in Swift, strings are value types. Changing B will not affect A b/c it’s a value type. A and B are different variables. Therefore, they are logically distinct.
Okay. Why shouldnt the arrays behave exactly the same way? So Array are value types in Swift. Dictionaries are also value types. You put value types and you get value type back.
All Swift “fundamental” types are values types (Int, Double,String,…). And the behaviour is the same = two vards are logically distinct.
All Swift collections built on them and also are value types. (Array, Set, Dictionary, …)
Swift tuples, structs, and enums that contain value types are value types.
The great thing here is that value types compose beautifully. So you can build up very rich abstactions all in the world of value semantics easily.
There is one more critical piece of a value type:
You have the notion of when two values, two variables of value type, are equivalent. They hold the same value. And the important thing is that identity is not what matters.
Because you can have any number of copies. What matters is the actual value that’s stored there. It does not matter how you got to that value.
I will tell you things that are really, really silly. Here we have A, we set it to 5, and we have B and we set it to 2 plus 3.
var a: Int = 5 var b: Int = 2 + 3 assert(a == b)
Of course A and B are equivalent. You work with this all the time. You counld not understand integers if they did not work this way. So just extend that notion out. Of cource its the same thing for CGPoints. Why shouldnt strings behave exactly the same way? The string is the value, and the equality operator needs to represent that. Arrays are equal if all value types + their positions are equal
Equitable
When you are building a value type, its extremely important that it conforms to the Equitable protocol. B/c every value type out there should be equitable. That means it has the == operator to do a comparison, but that operator has to behave in a sensible way.
protocol Equitable { // Reflexive - “x == y” is “true” // Symmetric - “x == y” then “y == x” // Transitive - “x == y” and “y == z” then “x == z” func == (lhs: Self, rhs: Self) -> Bool
}
It needs to be reflexive, symmetric, and transitive. Why are these properties important? B/c you can not understand your code unless you have them.
var a = … var b = a assert(a == b) aseert(b == a) var c = b assert(c == a)
If I copy from A to B, well, I expect that A is equal to B and B is equal to A. If I then copy B over to C, well, then C and B and A, they are all equivalent. It does not matter which I have b/c the only thing that matters there is the value, not the identity.
Fortunately, its very easy to implement these things.
extention CGPoint: Equitable { }
func == (lhs: Self, rhs: Self) -> Bool { return lhs.x == rhs.x && lhs.y == rhs.y }
Generally you would use underlying == operators of all the value types.
We gonna switch it back to var so we can mutate it.
struct Temperature: Equatable { var celsius: Double = 0 var fahrenheit: Double { get { return celsius * 9 / 5 + 32 } set { celsius = (newValue - 32) * 5 / 9 } } }
func == (lhs: Self, rhs: Self) -> Bool { return lhs.celsius == rhs.celsius }
let home = House() var temp = Temperature() // var and not let b/c we can not mutate let constant temp.fahrenheit = 75 home.thermostat.temperature = temp
temp.fahrenheit = 425 home.oven.temperature = temp home.oven.bake()
Why is it ok? The House points out to the thermostat in the oven. The thermostat and the oven both have their own instances of temperature values. They are completely distinct, they are never shared. They also happen to be inlined into the struct, and you are getting better memory usage and performance. Value semantics have made our lives easier here. Let’s go all the way and make everything value semantics. Now house it a struct that has a thermostat struct and an oven struct. The only thing we need to change is to make Home and Thermostat mutable.
This brings us to a really important point.
let means “the value will never change” var means you can update the value without affecting any other values Note that this change is very local. I can change this variable, but its not going to affect anything else anywhere in my program until I do a mutation somewhere else, which gives you this really nice controlled mutability.
var numbers = [1,2,3,4,5] scheduler.processNumbersAsync(numbers) for i in 0..<numbers.count { numbers[i] = numbers[i] * i } scheduler.processNumbersAsync(numbers)
With the ref semantics array, this is a race condition. With value semantics you are getting copies each time, logical (of values not pointers) copies each time. Therefore, there is no race condition. They are hitting the same array.
Now what about all those copies, isnt it a performance problem? We are doing a copy every time we pass numbers through a parameter.
One of the other important pieces of value semantics is that copies are cheap. Constant time cheap. Lets build this up from fundamentals.
Int, Double, etc low-level fundamental types copy is constant time. You are copying a couple of bytes. Usually it happens in the processor.
CGPoint, etc struct, enum, or tuple of value types is constant time. They have fixed number of fields, and copying each of the things in there is constant time. So copying the whole thing is constant time. That’s great for fixed-length (Int always take the same amount of space in memory) things.
Extensible data structures use copy-on-write. This makes the copy cheap. Copying involves a fixed number of reference-counting operations to do a copy of a copy-on-write value. And then at the point where you do a mutation, you have a var and then you change it, then we make a copy and work on the copy. So you have sharing behind the scenes, but it’s not logical sharing, logically these are still distinct values. This gives you great performance characteristics from value semantics and is a really nice programming model.
Value types in practice ———————–
We will build a simple diagram made of a couple of different value types, a circle and a polygon. We will start with the Circle.
struct Circle: Equitable { var center: CGPoint var radius: Double
init(center: CGPoint, radius: Double) {
self.center = center
self.radius = radius
}
}
func == (lhs: Self, rhs: Self) -> Bool { return lhs.center == rhs.center && lhs.radius == rhs.radius }
Since CGPoint and Double are built into Swift Standard Library we can just compare them as they already implement equality. Next up is the Polygon.
struct Polygon: Equitable { var corners: [CGPoint] = [] }
func == (lhs: Self, rhs: Self) -> Bool { return lhs.corners == rhs.corners }
Since array of corners is value type, the comparison is simple.
Now what we want to do is to put these types into our diagram. Making an array of either of those types (Circle, Poly) is streightforward. What we need to do is to make one array that contains both. The mechanism to do that in Swift is with a protocol. Protocols can abstract over value types.
So we will create a protocol called Drawable. And we will make both of our subtypes implement that protocol, and then we can put them into array in our diagram.
Here is Drawable
protocol Drawable { func draw() }
Staightforward and simple, has one method, Draw, on it.
…
This is the same example as in Crusty session.
Mixing value types and reference types ————————————–
In Obj-C, you ate used to putting primitive data types inside your reference types all the time. This is how we build things in Obj-C.
class Button: Control { var label: String var enabled: Bool }
But the flip side
struct ButtonWrapper { var button: Button }
introduces some interesting questions that we have to think about:
Lets start with a simple example with an immutable class, UIImage
struct Image: Drawable { var topLeft: CGPoint var image: UIImage }
Using Immutable Reference ————————-
var image = Image(topLeft: CGPoint(x:0,y:0), image: UIImage(imageNamed:”San Francisco”)!) var image2 = image
We create 2 instances and now image and image2 both pointing to the same UIImage object. Looking at this, it looks like we are going to have a problem, and it will be like a temperature example. But its not b/c UIImage is immutable. So we dont have to worry about image 2 mutating the image that sits underneath it and having the first image be caught off guard with that change.
Of course we want to make sure we implement the equality
func == (lhs: Image, rhs: Image) -> Bool { return lhs.topLeft == rhs.topLeft && lhs.image === rhs.image }
You might think to use tripple equals to compare the references, this would work ok in this example, but we also have to think through what happens if we create two UI images using the same underlying bitmap. In the above case it will falsly say that these two images are not the same.
So instead what we want to do is use the isEqual method that we inherit from NSObject on UIImage to do the comparison, so we`ll be sure that the ref. type gets the right answer on whether or not its the same object or not.
Using Mutable Reference ———————–
struct BezierPath: Drawable { var path = UIBezierPath() var isEmpty: Bool { return path.empty }
func addLineToPoint(point: CGPoint) {
path.addLineToPoint(point)
} }
The entire implementation of our BezierPath is made up by this mutable ref. type, UIBezierPath. In the reading case when we are doing isEmpty, everything is ok. We are not doing any mutation. But in addLineToPoint method, if we have 2 BezierPaths pointing to that, it will cause problems. Also notice here, we dont have the Mutating keyword there. That’s a sign that we know we are mutating b/c AddLineToPoint is there, but the compiler is not yelling at us about it. That’s b.c. path is a ref. type.
So if I have two instances of BezierPath, both pointing to the same UIBezierPath through this reference, and I make this mutation, thats going to catch the other one off guard. We are not maintaining value semantics.
The way we will fix that is use copy-on-write
Unrestricted mutation of ref objects breaks value semantics We separate non-mutating operations from mutating ones
To fo that, we need to introduce a couple of new things to our BezierPath.
struct BezierPath: Drawable { private var _path = UIBezierPath() var pathForReading: UIBezierPath { return _path }
var pathForWriting: UIBezierPath {
mutating get {
_path = _path.copy() as! UIBesierPath
return _path
}
}
var isEmpty: Bool {
return pathForReading.empty
}
func addLineToPoint(point: CGPoint) {
pathForWriting.addLineToPoint(point)
}
}
First, we want to make our path instance private, and next we want to implement computed property for reading. Then we add variable for writing computed property thats marked as mutating, and that is going to change the state.
Now we have reading copy and a way to get a writing copy.
Now in the isEmpty we call our reading copy, and in the mutating method below, we will call the path for writing. And the great thing about this is that compiler is going to yell at us and say “Hey, that pathForWriting property is marked as mutating, and this method is not”. So we are getting hekp from the compiler to help us figure out when we are doing something wrong.
mutating func addLineToPoint(point: CGPoint) { pathForWriting.addLineToPoint(point) }
Let’s how it works
var path = BezierPath() var path2 = path
We create 2 references to the same UIBezierPath. We can read it w/o problem
if path.isEmpty { return }
When I go to write to it, since Im creating another instance of BezierPath, path2 does not care if mutation has happened. So I will not introduce some unexpected mutation behind path2.
path.addLineToPoint( CGPoint(x:10,y:20) )
Let’s talk how to use these things in practice.
extension Polygon { var path: BezierPath { var result = BezierPath() result.moveToPoint(corners.last!) for point in corners { result.addLineToPoint(point) } return result } }
The downside is as we remember that AllLineToPoint is copying on every call. So its not going to perform as well as it might. So instead, what we should do is create an instance of UIBezierPath and mutate that mutable reference type in place. And when you are done, create a new instance of our value type with that BezierPath and return that.
extension Polygon { var path: BezierPath { var result = UIBezierPath() result.moveToPoint(corners.last!) for point in corners { result.addLineToPoint(point) } return BezierPath(path:result) } }
That creates only one copy of only one instance of UIBezierPath instead of multiple.
In Swift, we have a great feature where we know if objects are uniquely referenced.
struct MyWrapper {
var _object: SomeSwiftObject
var objectForWriting: SomeSwiftObject {
mutating get {
_object = _object.copy()
return _object
}
}
}
So we can take advantage of that. We can use the fact that we have this uniquely referenced property and we know for a fact that something is uniquely referenced so we can avoid making the copies if we know that that ref type is uniqiely referenced.
struct MyWrapper {
var _object: SomeSwiftObject
var objectForWriting: SomeSwiftObject {
mutating get {
if !isUniquelyReferencedNonObjC(&_object) {
_object = _object.copy()
}
return _object
}
}
}
The Standard Lib uses that feature throughout and does a lot of great performance optimizations using that.
Main points:
Now lets look at a really cool feature we can do now that we have a model type implemented as a value
var doc = Diagram() var undoStack: [Diagram] = []
With every mutation, I will add my odc to my diagram array
undoStack.appeand(doc)
doc.addItem(Polygon()) undoStack.append(doc)
doc.addItem(Cicle()) undoStack.append(doc)
Now in my undo stack I have 3 distinct instances of Diagram. These are not references to the same thing, these are 3 distinct values. And so, I can imlement some really cool features with this. So, imagine this in an app and I have a History button. When I tap on the History button and I get the list of all the states. I can allow user to tap on something and essentially go back in time.
I dont have to keep anything in some array of how to undo adding that property or anything. It just goes back to that prev. instance, and that’s the one that gets drawn.
This is a super-powerful feature, and , in fact, Photoshop uses this extensively to implement all of their history stuff. What happens behind the scene?
Photoshop slices and dices that photo, no matter how large it is, into a bunch of small tiles. Each of those tiles are values, and the document that contains the tiles is also a value. Then if I make a change, like change this person’s shirt from purple to green, the only thing that gets copied in the two instances of that diagram are the tiles that contain the persons shirt. So even though I have 2 distinct documents, the old state and the new state, the only new data that I have had to consume as a result of that is the tiles contained in this person’s shirt.