To take a concrete example, consider the undo-redo mechanism in a text editor. It seems natural to create a list of objects with each object representing the action that can be undone or redone, and a pointer to the most recent action object. Undoing executes the 'undo' method and moves the pointer to the previous action. Redoing moves the pointer to the next action and calls it's 'redo' method.
You would have a 'delete text' type of action which knows how to remove a particular chunk of text and put it back. You would have a 'change text colour' action which knows how to change text to a particular colour and revert back. You might have an 'insert jpeg' action which knows how to add a picture and remove it from the document.
Crucially, each action object in the list should encapsulate it's data and behaviour. It makes the undo-redo framework so much simpler if the framework knows nothing about each type of action, besides the fact that each action object has some kind of undo and redo behaviour.
I cannot see how you could get a simpler design without OO principles such as coupling the data and methods of each type of action, or without hiding the implementation details of each action from the undo-redo framework.
Most software with undo-redo will show you some metadata related to the action (for instance "Undo 'Drag stuff'", "Redo 'Set text in bold'"...) so you have to have a way to associate metadata with the "undo / redo" functions; the class is a good tool for this.
Also, a common optimization is to alter the head of the stack instead of deleting / creating new commands when for instance you are moving an object in the GUI. e.g. say you have a command that moves a box at a specific position. If command objects are immutable, every time the mouse moves, you have to pop the head of your stack and push a new command instead (which may be a waste of resources); instead you can just have an "update" method which will change the position stored inside the command which is microscopic in comparison of malloc / free.
Have you profiled that?! :)
Seriously though, design wise there was not much you said that was specific to OO. Take for example:
- all actions are a data type
- the history is a list of actions (in LIFO style for the purpose of simplicity here)
- undo pops the list and puts undo in another list
- redo replays the undo list onto the history list
A bit more outlined, but nothing OO there, it was in fact entirely functional.
actions = [append("The"), append(" quick"), append(" brown"), append(" fox")]
state = reduce(actions, some_combiner) // This might give you a string "The quick brown fox"
If you pop the last element `append(" fox")` of the actions and rebuild the state, you can easily see that the new state "The quick brown" is simply the old state without the last action, i.e. an "undo".If rebuilding the state is expensive, it is not hard to devise caches around state management under this model.
However, under this model, you really don't want your actions in the actions list to change after the fact. That means those actions in the list really shouldn't be modeled as "objects" with object-like mutable internal state and all. In fact, you'd probably need them to be as lightweight as possible, which means no sticking methods on them.
With those constraints in mind, OO-style code brings nothing to the table.
FP brings a lot of interesting concepts to the table that are useful. But I doubt that people that screw up OOP will screw up less with FP
In the end competent and disciplined (in my view the number one attribute of a good dev) people will produce good software with any paradigm. Clueless people will cause problems no matter what the tool is.
Caching becomes a lot easier though in a pure functional programming lang though, and in a lazy one happens automatically for a lot of things.
My code is pretty exactly as you describe, an undo and redo stack holding action objects and the interfaces to the action objects being nothing more than 'undo()' and 'redo()' methods.
Undo = [ { RemoveJpeg, JpegData} ]
where RemoveJpeg is a fun (like closure) and JpegData is what would be given as argument to this fun. The implementing undo looks like this: undo_action(Document,[UndoHead|UndoTail]) ->
{UndoFun,UndoData} = UndoHead,
UndoFun(Document,UndoData).
Isn't it simple? RemoveJpeg just returns modified document. undo_action knows only that every action should take two arguments and return modified document. RemoveJpeg <- [:data | ...].
JpegData <- [:data | ...].
Undo <- #( RemoveJpeg, JpegData ).
As possible implementation of undo could be undoAction: document with: undoList
undoList do: [:aBlock|
aBlock value: document].The same would have happened with FP. People would have messed it up too.
No one stops you writing program in whatever style you want. If functional programming has that of edge over OO, it would win over the argument long ago.
I don't like you assume other people don't know what is best for themselves, and needs some top-down expert to light the path. You argument, if there is one, sounds really preachy and proves no other than the point that support of functional programming here in HN are more about identity politics than actually making it useful to the crowd.
I do love the juxtaposition of this line, with your following accusation of people on HN engaging in "identity politics".
In my view the argument "X can't be better than Y, otherwise we'd all be using X". Is wrong for the same reason that "K can't be a good idea for a company to start otherwise someone would've started it already."
Some things are too early, some things are marketed poorly, sometimes it's luck, some times is dedication in sticking with it. I strongly believe that markets aren't efficient and have lots of imbalances. I also strongly believe that life isn't efficient and has lots of imbalances.