Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add last / prepend to CircularBuffer #347

Merged
merged 1 commit into from
Apr 24, 2018
Merged

Conversation

normanmaurer
Copy link
Member

@normanmaurer normanmaurer commented Apr 23, 2018

Motivation:

Its often useful to be able to add / retrieve elements on both sides of a CircularBuffer.

Modifications:

  • Add last / prepend to be able to act on both ends.
  • Add tests

Result:

Fixes #279

@normanmaurer normanmaurer requested review from Lukasa and weissi and removed request for Lukasa April 23, 2018 10:30
@normanmaurer
Copy link
Member Author

FYI @helje5

@helje5
Copy link
Contributor

helje5 commented Apr 23, 2018

Very nice! 👯 Should that also do the power-of-2 buffer size increases instead of doubling? For Redi/S to be fast, I had to do manual capacity increases.

@helje5
Copy link
Contributor

helje5 commented Apr 23, 2018

Oh, I just noticed you already did that in #313 .

} else {
return self.buffer[self.headIdx]
}
// As self.buffer is filled with `nil` this will correctly return `nil` if the CircularBuffer is empty.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change worth it? The path of checking isEmpty is going to be faster, because it saves us the additional level of indirection into the buffer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa not sure I think it is a nice change but I am not strong on it...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weissi for the tie-break!

Copy link
Member

@weissi weissi Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels too clever to me, I'd leave the old code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weissi achievement unlocked... @weissi called me clever =P

Let me revert this part

@normanmaurer
Copy link
Member Author

@weissi @Lukasa PTAL again

@Lukasa Lukasa added the 🆕 semver/minor Adds new public API. label Apr 23, 2018
@Lukasa Lukasa added this to the 1.6.0 milestone Apr 23, 2018
Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks really good but I think the naming of the functions should be improved. Also paged the stdlib guys so they can help us out here.

/// Add an element to the end of the ring buffer.
///
/// Amortized *O(1)*
public mutating func addLast(_ value: E) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have this method? We have append already which matches Array's append.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weissi I added it for consistent naming ;)

/// Add an element to the front of the ring buffer.
///
/// Amortized *O(1)*
public mutating func addFirst(_ value: E) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift doesn't really seem to use add.... I would suggest prepend maybe as it's the opposite of append? CC @lorentey, @moiseev, @airspeedswift

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also: what protocols should this auto expanding ring buffer implement?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending to the front gets us to BidirectionalCollection. Now that we're at that, we can actually hop the next level up to RandomAccessCollection. Given the way this behaves, we could also give it RangeReplaceableCollection too, which is a pretty easy cousin of RandomAccessCollection.

So I'd recommend adding conformances to those two, though we may want to do them in a different PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa isn't RangeReplaceableCollection a thing where you can replace any range by any other range? So for example put 5 elements in a space where there used to be 3 ?

Like thing[1...3] = ["foo", "bar", "buz", "qux", "quux"] ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, and our buffer can do that. It's no less efficient at it than a regular array.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that (startIndex=0) wouldn't matter much. (though Redi/S lists also have that getrange-family of operations, but that could be mapped in the frontend).

The thing I'm wondering is whether there should also be a prepend(contentsOf:) which reserves the proper capacity.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can see, CircularBuffer does not have even an append(contentsOf:) right now.

I had this idea for a moment, but then I quickly hit a wall trying to name prepend(contentsOf:) correctly and specify the behavior when tailIdx reaches the headIdx, so I decided not to =)

Going back to indices. FWIW, in the collection types provided by the standard library there are no guarantees about the values of indices, so one should always use startIndex/endIndex combo and index family of methods, which might be annoying, but is the only guaranteed way of writing generic code over collections, even when Index: FixedWidthInteger or Index == Int.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moiseev Many thanks! I'm not personally attached to the 0 based indexing and I think we can change it as SemVer patch I think. What do @normanmaurer & @Lukasa think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and obvs I'm fine with prepend

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weissi sounds good to me... That said I think we can pull in the PR as it is now... WDYT ?

let value = self.buffer[self.headIdx]
self.buffer[headIdx] = nil

precondition(value != nil, "CircularBuffer is empty")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be the first instruction I think, just check isEmpty

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weissi sure I can do this but I would argue that this way we it may be a bit faster...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want a speedup, the better thing to do is to do this:

guard let value = self.buffer[headIdx] else {
    preconditionFailure("Circular buffer is empty")
}
self.buffer[headIdx] = nil
self.headIdx = (self.headIdx + 1) & self.mask
return value

This checks the optional only once, not twice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine with me as well...(would use fatalError tho) @weissi ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use fatalError?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa actually you are right preconditionFailure may be more correct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

happy with either

let idx = (self.tailIdx - 1) & self.mask
let value = self.buffer[idx]

precondition(value != nil, "CircularBuffer is empty")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be first

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public mutating func removeLast() -> E {
let idx = (self.tailIdx - 1) & self.mask
guard let value = self.buffer[idx] else {
fatalError("CircularBuffer is empty")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think preconditionFailure here is fine, right @weissi?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep I think you are right...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, happy with either

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌 thanks, looks great

Motivation:

Its often useful to be able to add / retrieve elements on both sides of a CircularBuffer.

Modifications:

- Add last / prepend to be able to act on both ends.
- Add tests

Result:

Fixes apple#279
@normanmaurer normanmaurer changed the title Add last / addFirst to CircularBuffer Add last / prepend to CircularBuffer Apr 24, 2018
@normanmaurer
Copy link
Member Author

Will merge one the CI completes

@normanmaurer normanmaurer merged commit 976074d into apple:master Apr 24, 2018
@normanmaurer normanmaurer deleted the deque branch April 24, 2018 08:26
@normanmaurer
Copy link
Member Author

Created #350 as a followup.

esttorhe added a commit to esttorhe/swift-nio that referenced this pull request Jun 3, 2018
Motivation:

During the `PR` apple#347 there was a discussion around which
protocols this autoexpanding buffer should conform; the following 3
being the winners:

1. BidirectionalCollection
2. RandomAccessCollection
3. RangeReplaceableCollection

Modifications:

Added conformance to the 3 protocolos and provided implementaiton for 4
methods:

1. replaceSubrange(:with:)
2. index(before:)
3. removeSubrange(:)
4. remove(at:)
5. removeLast(:)

Result:

With the conformance of this 3 protocols and the implementation of this
5 functions we get dropLast(), dropLast(:), removeFirst(), removeLast()
& popLast() for free.
esttorhe added a commit to esttorhe/swift-nio that referenced this pull request Jun 3, 2018
Motivation:

During the `PR` apple#347 there was a discussion around which
protocols this autoexpanding buffer should conform; the following 3
being the winners:

1. BidirectionalCollection
2. RandomAccessCollection
3. RangeReplaceableCollection

Modifications:

Added conformance to the 3 protocolos and provided implementaiton for 4
methods:

1. replaceSubrange(:with:)
2. index(before:)
3. removeSubrange(:)
4. remove(at:)
5. removeLast(:)

Result:

With the conformance of this 3 protocols and the implementation of this
5 functions we get dropLast(), dropLast(:), removeFirst(), removeLast()
& popLast() for free.
esttorhe added a commit to esttorhe/swift-nio that referenced this pull request Jun 4, 2018
Motivation:

During the `PR` apple#347 there was a discussion around which
protocols this autoexpanding buffer should conform; the following 3
being the winners:

1. BidirectionalCollection
2. RandomAccessCollection
3. RangeReplaceableCollection

Modifications:

Added conformance to the 3 protocolos and provided implementaiton for 4
methods:

1. replaceSubrange(:with:)
2. index(before:)
3. removeSubrange(:)
4. remove(at:)
5. removeLast(:)

Result:

With the conformance of this 3 protocols and the implementation of this
5 functions we get dropLast(), dropLast(:), removeFirst(), removeLast()
& popLast() for free.
esttorhe added a commit to esttorhe/swift-nio that referenced this pull request Jul 11, 2018
Motivation:

During the `PR` apple#347 there was a discussion around which
protocols this autoexpanding buffer should conform; the following 3
being the winners:

1. BidirectionalCollection
2. RandomAccessCollection
3. RangeReplaceableCollection

Modifications:

Added conformance to the 3 protocolos and provided implementaiton for 4
methods:

1. replaceSubrange(:with:)
2. index(before:)
3. removeSubrange(:)
4. remove(at:)
5. removeLast(:)

Result:

With the conformance of this 3 protocols and the implementation of this
5 functions we get dropLast(), dropLast(:), removeFirst(), removeLast()
& popLast() for free.
esttorhe added a commit to esttorhe/swift-nio that referenced this pull request Jul 11, 2018
Motivation:

During the `PR` apple#347 there was a discussion around which
protocols this autoexpanding buffer should conform; the following 3
being the winners:

1. BidirectionalCollection
2. RandomAccessCollection
3. RangeReplaceableCollection

Modifications:

Added conformance to the 3 protocolos and provided implementaiton for 4
methods:

1. replaceSubrange(:with:)
2. index(before:)
3. removeSubrange(:)
4. remove(at:)
5. removeLast(:)

Result:

With the conformance of this 3 protocols and the implementation of this
5 functions we get dropLast(), dropLast(:), removeFirst(), removeLast()
& popLast() for free.
esttorhe added a commit to esttorhe/swift-nio that referenced this pull request Jul 11, 2018
Motivation:

During the `PR` apple#347 there was a discussion around which
protocols this autoexpanding buffer should conform; the following 3
being the winners:

1. BidirectionalCollection
2. RandomAccessCollection
3. RangeReplaceableCollection

Modifications:

Added conformance to the 3 protocolos and provided implementaiton for 4
methods:

1. replaceSubrange(:with:)
2. index(before:)
3. removeSubrange(:)
4. remove(at:)
5. removeLast(:)

Result:

With the conformance of this 3 protocols and the implementation of this
5 functions we get dropLast(), dropLast(:), removeFirst(), removeLast()
& popLast() for free.
esttorhe added a commit to esttorhe/swift-nio that referenced this pull request Jul 11, 2018
Motivation:

During the `PR` apple#347 there was a discussion around which
protocols this autoexpanding buffer should conform; the following 3
being the winners:

1. BidirectionalCollection
2. RandomAccessCollection
3. RangeReplaceableCollection

Modifications:

Added conformance to the 3 protocolos and provided implementaiton for 4
methods:

1. replaceSubrange(:with:)
2. index(before:)
3. removeSubrange(:)
4. remove(at:)
5. removeLast(:)

Result:

With the conformance of this 3 protocols and the implementation of this
5 functions we get dropLast(), dropLast(:), removeFirst(), removeLast()
& popLast() for free.
Lukasa pushed a commit that referenced this pull request Jul 12, 2018
Motivation:

During the `PR` #347 there was a discussion around which
protocols this autoexpanding buffer should conform; the following 3
being the winners:

1. BidirectionalCollection
2. RandomAccessCollection
3. RangeReplaceableCollection

Modifications:

Added conformance to the 3 protocolos and provided implementaiton for 4
methods:

1. replaceSubrange(:with:)
2. index(before:)
3. removeSubrange(:)
4. remove(at:)
5. removeLast(:)

Result:

With the conformance of this 3 protocols and the implementation of this
5 functions we get dropLast(), dropLast(:), removeFirst(), removeLast()
& popLast() for free.
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants