-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
std/lists: O(1) concatenation of singly- and doubly linked lists. #16362
Conversation
Thanks! Please add |
I think these should be overloads of |
Added the annotations (I hope they're OK), the tests, and the changelog. |
Yes but |
Okay, I renamed it. But thinking about it, I found that |
Instead of "concat" you can use "&", but I don't like using prepend/append for it. |
I personally like the |
Well |
The problem with |
Almost all of stdlib uses
it's also what's recommended in nep1 explicitly: https://nim-lang.github.io/Nim/nep1.html (note: |
@timotheecour has a point there... if the others agree, I'll rename it to |
Indeed. However, why not instead add |
Because the whole point of this PR is to create an O(1) concatenation :) |
By the way, import lists, sugar
func `&`(L1, L2: SinglyLinkedList): SinglyLinkedList = L1.deepCopy.dup(add(L2.deepCopy)) ... or |
Ah, good one. |
I've just realized that this operation is not really meaningful for doubly linked lists, as it will always leave one in an inconsistent state. Consider var
a = [1, 2, 3].toDoublyLinkedList
b = [4, 5, 6].toDoublyLinkedList
c = a.add(b) If I set the I can't see a really good solution, so I removed the |
tests/stdlib/tlists.nim
Outdated
@@ -81,3 +81,39 @@ block tlistsToString: | |||
l.append('2') | |||
l.append('3') | |||
doAssert $l == """['1', '2', '3']""" | |||
|
|||
block SinglyLinkedListConversion: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name the block after the symbols you're testing: toSinglyLinkedList
it makes it easier to search for tests for a given symbol
(note that merely using a symbol in some test isn't the same as a dedicated block testing it)
if a block tests multiple symbols, use:
block: # foo1, foo2
tests/stdlib/tlists.nim
Outdated
@@ -81,3 +81,39 @@ block tlistsToString: | |||
l.append('2') | |||
l.append('3') | |||
doAssert $l == """['1', '2', '3']""" | |||
|
|||
block SinglyLinkedListConversion: | |||
let l: seq[int] = @[] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let l = seq[int].default
tests/stdlib/tlists.nim
Outdated
|
||
block SinglyLinkedListConversion: | ||
let l: seq[int] = @[] | ||
doAssert $l.toSinglyLinkedList == "[]" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use toSeq for tests; in general it's more robust and independent of how something is rendered
doAssert [1].toSinglyLinkedList.toSeq == [1]
Actually there's a similar problem with
I have one, see RFC
see RFC, which explains that can't work and |
Revised the tests according to @timotheecour's comments. |
lib/pure/collections/lists.nim
Outdated
var a = [1, 2, 3].toSinglyLinkedList | ||
let b = [4, 5].toSinglyLinkedList | ||
let a = [1, 2, 3].toSinglyLinkedList | ||
assert $a == $a.copy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you expand on this example to illustrate the semantics of shallow copying? eg using a ref object as in nim-lang/RFCs#303 (comment) or something better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
lib/pure/collections/lists.nim
Outdated
|
||
proc add*[T](L1, L2: var SinglyLinkedList[T]) {.since: (1, 5).} = | ||
## Moves `L2` to the end of `L1`. Efficiency: O(1). | ||
## Note that `L2` becomes empty after the operation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs to explain what happens for a.add a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It becomes empty, with the current implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
empty sucks though;
how about raising instead? ideally would be disallowed at CT (like rust) but for now raising might be best we can do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can do that, but I don't know ... somehow I feel that we concentrate on a special case too much. A lot worse than empty can happen when someone starts shuffling head
s and tail
s, even when sticking with the API's append
. (You can do a.append(a.head)
, after all, which results in a list of just the first element.)
I'm just wondering how far we should intervene... I'm all for liberty :)
lib/pure/collections/lists.nim
Outdated
b = a.copy | ||
f.x = 42 | ||
assert a.head.value.x == 42 | ||
assert b.head.value.x == 42 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add this or similar to emphasize the copy semantics. Makes behavior even more crystal clear.
(requiers changing a to var)
a.add [f].toSinglyLinkedList
doAssert a.toSeq == [f, f]
doAssert b.toSeq == [f] # b isn't modified...
f.x = 42
assert a.head.value.x == 42
assert b.head.value.x == 42 # ... but elements are not deep copied
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you insist... but (at least for me) the immutable argument in the declaration is enough to be sure that b
is not modified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the immutable argument in the declaration is enough to be sure that b is not modified
unfortunately that's not the case, see #16312 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand ... from that thread I see that an assignment can make a (bad) shallow copy, and mess up things. But surely a func
such as this copy
cannot do that?
... or maybe it can. But - to quote you - this is really bad.
tests/stdlib/tlists.nim
Outdated
l2.add(l1) | ||
doAssert l3.toSeq == [4, 5, 6] | ||
doAssert l2.toSeq == [2, 3, 1] | ||
block: # DoublyLinkedList |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a ton of duplication here. instead use this:
template testCommon(initList, toList) =
var
l0 = initDoublyLinkedList[int]()
l1 = [1].toList
l2 = [2, 3].toList
l3 = [4, 5, 6].toList
l4 = l3.copy
l5 = l3.copy
l0.add(l3)
l1.add(l4)
l2.add(l5)
testCommon(initDoublyLinkedList, toDoublyLinkedList)
testCommon(initSinglyLinkedList, toSinglyLinkedList)
it guarantees equal coverage, and simplifies maintenance etc
tests specific to doubly or singly can be in a different block, or, if it's a small diff, you can always use:
when type(l3) is DoublyLinkedList: ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, that will improve the code a lot, I was also bothered by the code duplication, but templates didn't occur to me.
tests/stdlib/tlists.nim
Outdated
doAssert l3.toSeq == [4, 5, 6] | ||
doAssert l2.toSeq == [2, 3, 1] | ||
testCommon(initSinglyLinkedList, toSinglyLinkedList) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you'd have less boilerplate with a single testCommon
, and sub-blocks for each common proc
block: # SinglyLinkedList, DoublyLinkedList
template testCommon(initList, toList) =
block copy: ...
block add: ...
...
testCommon(initSinglyLinkedList, toSinglyLinkedList)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also considered that, but I assumed that a single top-level block for each tested procedure was the standard :)
6b1a49b
to
37f6299
Compare
changelog.md
Outdated
@@ -65,6 +65,27 @@ | |||
|
|||
- `echo` and `debugEcho` will now raise `IOError` if writing to stdout fails. Previous behavior | |||
silently ignored errors. See #16366. Use `-d:nimLegacyEchoNoRaise` for previous behavior. | |||
- Added `lists.toSinglyLinkedList` and `lists.toDoublyLinkedList` for conversion from |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
changelog needs to be fixed (reason for duplication is
/changelog.md merge=union
, but it's there for a good reason) -
group everything for this PR under just 1 entry
-
newline between preceding entry (
echo and debugEcho...
) and your new entry
lib/pure/collections/lists.nim
Outdated
@@ -178,6 +179,28 @@ proc newSinglyLinkedNode*[T](value: T): <//>(SinglyLinkedNode[T]) = | |||
new(result) | |||
result.value = value | |||
|
|||
func toSinglyLinkedList*[T](elems: openArray[T]): SinglyLinkedList[T] {.since: (1, 5, 1).} = | |||
## Creates a new `SinglyLinkedList` with the members of the | |||
## given collection (seq, array, or string) `elems`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's not the place to describe what openArray is; it would violate "one definition rule" for docs; note that openArray definition is 1 click away thanks to docgen
=> simply:
Creates a new `SinglyLinkedList` from members of `elems`
(+ elsewhere)
doAssert [1].toList.toSeq == [1] | ||
doAssert [1, 2, 3].toList.toSeq == [1, 2, 3] | ||
|
||
block copy: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test doesn't actually test correctness of copy
(2 parts: source container is not modified; but elements are references)
(Remember that runnableExamples is only run for 1 specific backend, unlike this which will be tested on all backends (eventually, once #16384 is fixed))
EDIT: how about this:
type Foo = ref object
x: int
let f0 = Foo(x: 0)
let f1 = Foo(x: 1)
var a = [f0]. toList
var b = a.copy
b.append f1
doAssert a.toSeq == [f0]
doAssert b.toSeq == [f0, f1]
lib/pure/collections/lists.nim
Outdated
a.tail = b.tail | ||
if a.head == nil: | ||
a.head = b.head | ||
b.head = nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
change to:
if a.addr != b.addr:
b.head = nil
b.tail = nil
and add this test:
import lists, sequtils
import std/enumerate
import std/sugar
var a = [0,1].toSinglyLinkedList # actualy, `toList` in your test
a.addMoved a
let s = collect:
for i, ai in enumerate(a):
if i >= 6: break
ai
doAssert s == [0,1,0,1,0,1]
and edit doc comment to:
## Note that `b` becomes empty after the operation.
## Self-adding results in an empty list.
=>
## Note that `b` becomes empty after the operation unless it has same address as `a`.
that's IMO a saner behavior
- ditto with SinglyLinkedList
lib/pure/collections/lists.nim
Outdated
let c = [1, 2, 3].toSinglyLinkedList | ||
assert $c == $c.copy | ||
result = initSinglyLinkedList[T]() | ||
for x in a: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
a.items
is needed until for loop invoked from generic procedure defined in another module cant finditems
iterator #11167 is fixed
(ie same workaround as done in packedsets fix regression introduced in #15564 #16060) -
copy also needs this fix
Added some extra tests.
Done... btw, why can't I write |
=> followup in timotheecour#473 to avoid hijacking this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. thanks for your patience!
And thank you for your comments, I learned a lot in the process :) |
* O(1) concatenation of singly- and doubly linked lists. There is also new `toSinglyLinkedList` and `toDoublyLinkedList` functions for conversion from `openArray`s, similarly to `toHashSet` or `toTable`. * Add `sequtils` import to runnable examples with `toSeq`. * Added missing call to runnable examples. * Add .since annotation, changelog, and tests. * Rename `lists.concat` as an overload to `lists.append`. * Renamed `append` to `add` in lists. * Remove faulty `add` for `DoublyLinkedList`s. * Improved tests for lists. * `lists.add` moves the second list; added `lists.copy` for shallow copy. * More tests for `lists.add` and `lists.copy`. * More compact tests for lists with templates. * List concatenation with copying (`add`) and moving (tentatively `addMove`) * Renamed `addMove` to `addMoved`; renamed arguments according to the style guide. * Added extended example to `lists.copy`. * Corrected .since annotations to 1.6 * Add .since annotation, changelog, and tests. * Rename `lists.concat` as an overload to `lists.append`. * Renamed `append` to `add` in lists. * Remove faulty `add` for `DoublyLinkedList`s. * `lists.add` moves the second list; added `lists.copy` for shallow copy. * More tests for `lists.add` and `lists.copy`. * List concatenation with copying (`add`) and moving (tentatively `addMove`) * Renamed `addMove` to `addMoved`; renamed arguments according to the style guide. * Since declarations changed to (1,5,1). * Add .since annotation, changelog, and tests. * Rename `lists.concat` as an overload to `lists.append`. * Renamed `append` to `add` in lists. * Remove faulty `add` for `DoublyLinkedList`s. * `lists.add` moves the second list; added `lists.copy` for shallow copy. * More tests for `lists.add` and `lists.copy`. * List concatenation with copying (`add`) and moving (tentatively `addMove`) * Renamed `addMove` to `addMoved`; renamed arguments according to the style guide. * Changelog update. * Fix rebasing errors. * Self-adding with `lists.addMove` results in a cycle now. Added some extra tests.
* O(1) concatenation of singly- and doubly linked lists. There is also new `toSinglyLinkedList` and `toDoublyLinkedList` functions for conversion from `openArray`s, similarly to `toHashSet` or `toTable`. * Add `sequtils` import to runnable examples with `toSeq`. * Added missing call to runnable examples. * Add .since annotation, changelog, and tests. * Rename `lists.concat` as an overload to `lists.append`. * Renamed `append` to `add` in lists. * Remove faulty `add` for `DoublyLinkedList`s. * Improved tests for lists. * `lists.add` moves the second list; added `lists.copy` for shallow copy. * More tests for `lists.add` and `lists.copy`. * More compact tests for lists with templates. * List concatenation with copying (`add`) and moving (tentatively `addMove`) * Renamed `addMove` to `addMoved`; renamed arguments according to the style guide. * Added extended example to `lists.copy`. * Corrected .since annotations to 1.6 * Add .since annotation, changelog, and tests. * Rename `lists.concat` as an overload to `lists.append`. * Renamed `append` to `add` in lists. * Remove faulty `add` for `DoublyLinkedList`s. * `lists.add` moves the second list; added `lists.copy` for shallow copy. * More tests for `lists.add` and `lists.copy`. * List concatenation with copying (`add`) and moving (tentatively `addMove`) * Renamed `addMove` to `addMoved`; renamed arguments according to the style guide. * Since declarations changed to (1,5,1). * Add .since annotation, changelog, and tests. * Rename `lists.concat` as an overload to `lists.append`. * Renamed `append` to `add` in lists. * Remove faulty `add` for `DoublyLinkedList`s. * `lists.add` moves the second list; added `lists.copy` for shallow copy. * More tests for `lists.add` and `lists.copy`. * List concatenation with copying (`add`) and moving (tentatively `addMove`) * Renamed `addMove` to `addMoved`; renamed arguments according to the style guide. * Changelog update. * Fix rebasing errors. * Self-adding with `lists.addMove` results in a cycle now. Added some extra tests.
There is also new
toSinglyLinkedList
andtoDoublyLinkedList
functions for conversion from
openArray
s, similarlyto
toHashSet
ortoTable
.