Undo
Wednesday, April 22, 2009 2:15:12 AM
I figured out there are three ways of doing this.
1. Save the state of the entire Project V network (including the GUI) before each command and store it in a stack.
Advantages: Easy to implement.
Disadvantages: Takes huge amounts of memory and is slow as hell. May not even work if too many undo's are stored.
2. Have commands be able to undo what they did on their own. This way, I can store the command itself in the stack and simply ask each command to undo itself whenever necessary.
Advantages: Easy to implement the stack and uses very little memory.
Disadvantages: If a command doesn't implement undo correctly, everything is FUBAR. It's also tedious to ask developers to implement their own undo every time. Some operations can be complex and wide reaching.
3. Create a diff between the old and new state and store that. With the diff, you can reverse the operations by applying the diff in reverse order.
Advantages: Doesn't use much memory.
Disadvantages: Is difficult to automatically track all the changes unless you temporarilly save the old state before any command. Tracking the changes themselves can also be tedious and error-prone.
Are there other ways of implementing undo?
I can think of one. Instead of working backwards, you work forward. So you save two copies of the state. The first state is called the original state, and the second state is called the final state. Also, let's assume that we want to store a maximum of 10 undo operations.
Each time we do an action, we update the final state and store the command into the undo QUEUE! If there are more than 10 commands in the undo queue, then we execute the first command in the queue on the original state. Then we delete that command from the queue.
What this does is that any time we want to undo, we simply execute all the commands in the undo queue onto a duplicate of the original state. But we make sure NOT to execute the last command. If we undo twice, we don't execute the last two commands, etc. This enables us to have a history of commands. We can even jump around.
The drawback here is that you need to store two states. I wish there was a way to cut down the size of the original state. Not sure how to do that.
I really want a way to have undo's be automatic. No way I'm gonna implement commands that have to undo themselves.
In case anyone cares about my main data structures, I have three.
1. Node (contains Name, ID, Value, forward sets and reverse sets)
2. Forward sets (contains nodes).
3. Reverse sets (contains nodes).
Oh, and sets are named too, but that shouldn't matter much. A reverse set is to know what points to the current node. For example, the "Base" set is a reverse set because it points backwards. The "Derived" sets points forward to derived nodes.
Think of reverse sets as backlinks. They are automatic.
Consider node A, B, C and X. X has all of A, B and C for base types. So A will have X in its Derived set. B will have X in its Derived Set. C will have X in its Derived Set. OTOH, X will have A, B and C in its reverse Base set. Usually, forward sets and reverse sets have identical names. But this is not a requirement. IOW, it defaults to the same name, but you can override it.
Now that I have events implemented, I can track any and all changes to the network. Still, it seems somewhat overwhelming of a task. I'll have to think about it for a while.
Oh, one last thing. A lot of commands have temporary changes. Meaning that these changes should not be tracked at all.


Vorlath # Wednesday, April 22, 2009 4:32:43 AM
Anonymous # Wednesday, April 22, 2009 2:28:45 PM
Vorlath # Wednesday, April 22, 2009 6:33:50 PM
Hmmm.... Maybe there's a way I could make it contiguous... Dunno.
I think I'll be able to implement undo. I need to create a custom data structure that does not directly link to the nodes in any way. I have to reference them by ID. And if ID's change, then I have to keep track of that too so that previous undo's don't get messed up.
All I gotta say is that I'm glad I implemented network events. Otherwise, I don't see how I could do this. Even now, I'm not entirely sure how this is gonna pan out.
Vladasvladas # Tuesday, April 28, 2009 1:18:51 PM
Your option Nr 2 is close to what I would suggest. But instead of saving commands (for further applying undo actions), I'd suggest to save the previous state of affected nodes, or whatever else (other memory structures). It's most probably that any command on the nodes in your system is applied only to one or two nodes. So saving the previous state of these nodes would be not much memory and time consuming.
In similar way PROGRESS before imaging works - it saves the previous image of the record(s) which would be affected by following transaction, together with the command (edit, delete, etc...).
I'm not speaking here about other actions, like IDE commands (save file, etc). There should be another techniques in use for those matters.
And I think, there would be nice to have 'after image' (roll-forward) feature in Project V too (in similar way).
Vorlath # Tuesday, April 28, 2009 8:49:03 PM
Right now, I'm thinking of using a separate data structure that can represent the nodes without actually linking to them. This could potentially be rather simple, except for the monitoring of changes. But that's where my event system comes in handy.
I will start implementing this soon. Probably in the next day or two once I've settled on the details. There is one thing that is annoying me is that I'm going to have to keep track of node creation as well. Before, this didn't matter because I only cared about nodes inserted into this network. But now, if you create a node, I need to undo that creation if need be.
Undo has got to be the most annoying feature to implement.
Actually, I'm thinking of tagging (caching) any node that is being modified as described above. When an action is complete, I simply compare all of the cached nodes with the ones they represent. If they are the same (the action on them was temporary), then I delete the cache for that node. Otherwise, I keep it. This would be a simple matter and is very easily implemented with OO mechanisms.
I'm making a lot of sacrifices here. I was thinking I could make everything small and efficient. But there are a lot of places that are getting somewhat larger than I anticipated. And slower too. Even so, I believe it will remain faster than if I took a different strategy in building Project V.
In any case, undo implementation begins in a couple days. There are a few details I want to work out. Plus, it's playoff season. So you know... Then again, there will probably be a break tomorrow, so I may start early.
After that, I need to implement a way to store Project V networks. I'm thinking of requiring a database. Unfortunately, almost everything in Project V is built up from name/value pairs and sets. So I'm not sure how to go about building a proper database schema. I could simply serialize them all and have a list of ID's and data. I know this is basically name/value pairs, but it would be simple and it would work. Even if it's slow, I don't really care that much. It can be upgraded at a later time since the data structures are so simple.
Robbert van Dalenrapido # Wednesday, April 29, 2009 8:57:42 AM
Immutable structures (aka versions) could even be mixed/merged, making them strictly more powerful than, say, the Command pattern.
Efficient immutable map/array implementations exist, for example, with Finger Trees you can implement generic arrays fairly efficiently.
And Maps and arrays is all you need to implement *any* data structure.
Vorlath # Wednesday, April 29, 2009 11:23:43 PM
One issue is changing ID's. This doesn't happen too often, but it does happen. Mostly when defining interfaces.
So there are three areas that are giving me grief.
1. ID changes.
2. Node creation.
3. Node deletion.
Anonymous # Thursday, April 30, 2009 8:32:46 PM
Vorlath # Friday, May 1, 2009 3:22:23 PM
Node creation isn't tracked. And they're not actually values. The act of creation isn't something you can actually store. It's something you have to represent. If I undo this, I would have to make the node disapear. I can't just set some values somewhere. Same thing for deletion. But undoing a deletion isn't as bad since I can determine if a node exists or not. If not, then I can create it and fill in the data.
Ok, so immutable ID's and a few extra flags for creation and deletion would do the trick. I may as well add in another flag for GUI changes and another one for custom data. If a component wants to implement undo, they can simply store their data in a node. This will allow custom data to also implement undo by simply providing a method for creating, deleting and updating entities.
1. Create
2. Delete
3. Update
4. Compare (optional)
I'm thinking of providing a pure interface to allow implementation of those functions. In Project V applications, these will be implemented differently, but will also be accessible. If you implement them, Undo will be automatically available for whatever data you wish beyond what is automatically taken care of. So if you store anything outside of nodes, you can still use undo features.
StartCommand and EndCommand will also be available to set boundaries between undo levels. These can be nested so that you can merge commands together into bigger commands.
I don't see any problems with this, but it's a lot of work. Couple weeks should be enough I would think.
I was thinking about having a separate undo stack for each project, but this will cause problems with shared nodes (all nodes are meant to be shared in Project V). Which leads me to a really old problem I've been having. How do I sync changes in type definitions?
I could restrict type changes to when it's only used once (or not at all). But when testing, you often want to use it in multiple places, even in multiple projects. I've been battling with this issue for a really long time and haven't come up with a good solution. I think Microsoft had this problem with COM objects. Once an object is published, you can no longer change it. You have to create a new one.
If this were photoshop, it'd be like being able to use a graphic from within one image inside of another without duplicating it. How do you change the original and how do you only change the local instance? It'd be nice if I could duplicate everything, but that would do away with the notion of global interfaces which is crucial for Project V to work the way I want it.