Unnecessary Moves
kylerobson opened this issue · comments
Checklist
- This is not a Apple's bug.
- Reviewed the README and documents.
- Searched existing issues for ensure not duplicated.
Example Test Scenario
func testMovedIssue() {
let section = 1
let source1 = [1, 2, 3]
let target1 = [3, 2, 1]
// Test1: This test passes, but it should fail.
XCTAssertExactDifferences(
source: source1,
target: target1,
section: section,
expected: [
Changeset(
data: target1,
elementMoved: [
(source: ElementPath(element: 2, section: section), target: ElementPath(element: 0, section: section)),
(source: ElementPath(element: 1, section: section), target: ElementPath(element: 1, section: section)),
]
)
]
)
// Test2: This test fails, but it should pass.
XCTAssertExactDifferences(
source: source1,
target: target1,
section: section,
expected: [
Changeset(
data: target1,
elementMoved: [
(source: ElementPath(element: 2, section: section), target: ElementPath(element: 0, section: section)),
]
)
]
)
}
Expected Behavior
Test1 should fail, Test2 should pass.
Moves contains (2 -> 0), (1 -> 1).
Current Behavior
Test1 passes, Test2 fails.
Moves should contain (2 -> 0).
Steps to Reproduce
Add the test I wrote above and run the unit tests.
Detailed Description (Include Screenshots)
Moving the item from the same index to the same index should always be unnecessary. In general optimizing to make moves correct and minimal is preferred.
Environments
-
Library version: Master
-
Swift version: 4.2
-
iOS version: 12
-
Xcode version: 10.1
-
Devices/Simulators: DifferenceKit, "My Mac", CMD+U
-
CocoaPods/Carthage version: N/A
Hi @kylerobson
It looks like correct diff and testing result.
Applying only Move(2 -> 0) to source1
results in [3, 1, 2].
The source ElementPath
in the elementMoved
is index of source collection, and the target ElementPath
is index of target collection.
Even if the source and target indices in the Move are the same, it can't be omitted because it's affected by diffs other than Move.
I suppose what I'm reacting to is that it's possible to simply swap 3 and 1 in [1, 2, 3] -> [3, 2, 1]. If move is the only option, then two moves is the best we can do. If swap is an option, then one swap would suffice. Swap is a better option isn't it? It would avoid potentially two O(N) memory moves.
After the Move(2 -> 0) is applied, we have [3, 1, 2]
I could have up to three arrays:
[1, 2, 3] (source for the diff)
[3, 2, 1] (target for the diff)
[3, 1, 2] (current array after performing the first move)
How is Move(1 -> 1) meant to transform [3, 1, 2] to [3, 2, 1]?
Intuitively, I would receive Move(2, 1) or Move(1, 2) instead of Move(1, 1) so I can continue operating on the array being modified.
How do you actually expect people to use this?
swap
is intuitive but not practical because it's not supported by UIKit
or AppKit
API.
As already I commented, Move's source
index is that of the source array.
Therefore, [3, 1, 2] to [3, 2, 1] is the wrong explanation.
You can be understood if you think in the following steps.
- Decompose Move(2, 1) to delete(3, at: 2 of source array), insert(3, at: 0 of target array)
- Decompose Move(1, 1) to delete(2, at: 1 of source array), insert(2, at: 1 of target array)
- And apply all diffs to [1, 2, 3] simultaneously.
-> delete(3, at: 2 of source array): [1, 2]
-> insert(3, at: 0 of target array): [3, 1, 2]
-> delete(2, at: 1 of source array): [3, 1]
-> insert(2, at: 1 of target array): [3, 2, 1]
This work is probably similar to the internal implementation of perfromBatchUpdates
.
This theory is bit difficult to understanding, I coded it for repeatability testing here.
Closing due to inactivity.
Feel free to reopen if you still have a question.
Hi @ra1028 , i have two questions:
1
And apply all diffs to [1, 2, 3] simultaneously.
-> delete(3, at: 2 of source array): [1, 2]
-> insert(3, at: 0 of target array): [3, 1, 2]
-> delete(2, at: 1 of source array): [3, 1]
-> insert(2, at: 1 of target array): [3, 2, 1]
-> delete(3, at: 2 of source array): [1, 2]
: straightforward, right now source array is [1, 2].
-> insert(3, at: 0 of target array): [3, 1, 2]
: target array normal is [1, 2, 3] then insert 3 at pos 0, become [3, 1, 2, 3], delete idx 3 element, so target array is [3, 1, 2]
?
-> delete(2, at: 1 of source array): [3, 1]
: how can source array is [3, 2, 1]?
-> insert(2, at: 1 of target array): [3, 2, 1]
: target array normal is [3, 1, 2], insert 2 at pos 1, become [3, 2, 1, 2], delete idx 3 element, so target array is [3, 2, 1]
? If not i can't think of else possible case.
I don't understand what it means to operate on the target array? Isn't it possible to make source become target after performing a set of operations on source?
2
for example, source is [a, b, c, d], target is [b, a, d, c]. If use React element diff algorithm, we got steps:
1. b don't move;
2. a move after b;
3. d don't move;
4. c move after d;
source performs steps and then becomes target.
then i use same source and target in this algorithm, got changeSet's elementMoved
like below:
(source: [element: 1, section: 0], target: [element: 0, section: 0]) -> moveRow(IndexPath(1, 0), IndexPath(0, 0))
(source: [element: 3, section: 0], target: [element: 2, section: 0]) -> moveRow(IndexPath(3, 0), IndexPath(2, 0))
as same as steps like below?
move b before a;
move d before c;
If not, How can tableView use elementMove
update original elements to target elements? May be there is another way that differ from react's way(source execute steps then become target) let tableView updated?
please give some hint, thanks.
@ra1028 , please see above :)
1
My explanation maybe to have confused you.
delete(3, at: 2 of source array): [1, 2]
This means that the element 3 index for the source array is 2, and the deleted result is [1, 2]
.
insert(3, at: 0 of target array): [3, 1, 2]
Then, insert the element 3 to [1, 2]
, the index 0 is pointed to the position in the final target array. Result is [3, 1, 2]
delete(2, at: 1 of source array): [3, 1]
Delete the element 2 from [3, 1, 2]
. The index is pointed to the position of the source array before change. Result is [3, 1]
.
insert(2, at: 1 of target array): [3, 2, 1]
Finally, insert the element 2 to [3, 1]
. The index is pointed to the position of the final target array after changed. Result is [3, 2, 1]
.
You should think that it's strange because the index should have shifted by delete or insert.
But that is correct, because these steps are operated simultaneously, not sequentially, by performBatchUpdate
.
This is an important concept of the diffing algorithm and it's difficult for me to explain.
I recommend to read the algorithm code of DifferenceKit.
https://github.com/ra1028/DifferenceKit/blob/master/Sources/Algorithm.swift#L445
The code for reproducibility testing will maybe also help you understand.
https://github.com/ra1028/DifferenceKit/blob/master/Tests/TestTools.swift#L132
2
In that case, as you say, the steps are as follows:
1. a don't move;
2. b move before a;
3. c don't move;
4. d move before c;
These show different diffs, but the results are the same.
I'm not familiar with React algorithm, but I guess DifferenceKit and React are using different algorithms.
There are many types of diffing algorithms and detailed spec differences.
@ra1028 Thank you for your answer :)