[WIP] std.range: implement bitwise adapter over Integral type Ranges#4927
[WIP] std.range: implement bitwise adapter over Integral type Ranges#4927andralex merged 6 commits intodlang:masterfrom
Conversation
|
I don't have time to look at this in depth right now, but I would point out that it would be more appropriate for something like this to be in std.bitmanip. |
|
@jmdavis I was joking to @edi33416 that an older (Google?) paper argued that tag-based systems are better and more intuitive to humans than hierarchical systems. This seems to be a good anecdote supporting that. Bitwise defines a range interface on top of a range interface, so it's about |
|
@andralex Well, since a large portion of Phobos is just code that creates ranges of one sort or another (std.algorithm most notably), if the fact that something is a range puts it in std.range, a large portion of Phobos belongs in there. Obviously, it's sometimes non-obvious or debatable where something should go, but it seems that std.range has mostly been for range primitives and stuff that are building blocks for ranges, which this isn't. std.bitmanip, on the other hand, is where all of the bit manipulation / bitwise stuff has gone thus far. And since that's the sort of thing that this is doing, I think that it makes a lot more sense for it to be in std.bitmanip. So, while it's certainly debatable, I would have thought that it was fairly clear in this case, and I'm inclined to resist putting stuff in std.range just because it involves a range, particularly since ranges are so pervasive in Phobos and only becoming more so. |
andralex
left a comment
There was a problem hiding this comment.
Great work, ready for one more pass with an eye for minimalism.
std/range/package.d
Outdated
| } | ||
|
|
||
| /** | ||
| Bitwise adapter over Integral type Ranges. Consumes the range elements bit by bit. |
There was a problem hiding this comment.
Integral and Ranges in lowercase
std/range/package.d
Outdated
| if (isInputRange!R && isIntegral!(ElementType!R)) | ||
| { | ||
| alias ER = ElementType!R; | ||
| alias UER = Unsigned!ER; |
There was a problem hiding this comment.
We try to avoid acronyms that lead to multi-capitalized names. I suggest
private alias ElementType = ElementType!R;
private alias MaskType = Unsigned!ElementType;
std/range/package.d
Outdated
|
|
||
| private R parent; | ||
| private UER mask; | ||
| private int maskPos; |
There was a problem hiding this comment.
prefer uint for this kind of stuff
|
|
||
| private enum UER bitsNum = ER.sizeof * 8; | ||
|
|
||
| this()(auto ref R range) |
There was a problem hiding this comment.
Pass by value should suffice here, no?
There was a problem hiding this comment.
I used a pass by ref here in order to copy the range once instead of twice (first when it gets passed to the ctor and the second time when it gets copied into parent)
There was a problem hiding this comment.
The compiler should be eliding the copies anyway. You shouldn't need to worry about it.
std/range/package.d
Outdated
| } | ||
| } | ||
|
|
||
| bool empty() const |
There was a problem hiding this comment.
If you put const on this method you force parent.empty to also be const. If you leave no const, deduction will take care of it appropriately (making it const wherever possible).
There was a problem hiding this comment.
Unless something has changed recently, const is not inferred. So, if you want it to be const or not based on the template argument, then you have to use introspection and static if. That being said, it still shouldn't just be marked as const, because then it won't work with many ranges.
There was a problem hiding this comment.
const inferrence does have a workaround:
bool empty(this This)() { ... }This will only compile on a const wrapper range if all the calls can be made on the lower range. It's more crude than defining or not defining the method as const, but it would work.
However, my opinion is that const on ranges is useless anyway -- you can't iterate a const range. So I would recommend just not putting const here.
std/range/package.d
Outdated
| private void initPrivates() | ||
| { | ||
| // Helper function to avoid code duplication | ||
| mask = cast(UER)(1) << (bitsNum - 1); |
std/range/package.d
Outdated
| { | ||
| typeof(this) save() | ||
| { | ||
| return this; |
There was a problem hiding this comment.
Here you need to save parent as well:
typeof(this) save()
{
auto result = this;
result.parent = parent.save;
return result;
}
std/range/package.d
Outdated
|
|
||
| bool back() | ||
| { | ||
| return cast(bool)(backAccumulator & backMask); |
There was a problem hiding this comment.
to avoid using cast and triggering all greps for unsafe code, use return (backAccumulator & backMask) != 0;
Also, here you can use const on the method because you don't depend on any templated stuff.
std/range/package.d
Outdated
| } | ||
| else | ||
| { | ||
| mask <<= 1; |
std/range/package.d
Outdated
| import core.exception : RangeError; | ||
| import std.exception : enforce; | ||
|
|
||
| enforce(n < len, new RangeError); |
There was a problem hiding this comment.
Inside contracts always use assert, never use enforce
|
@jmdavis how about this: a close review of the next round of this code will amplify your voting rights by 10x :) |
|
(BTW I approve the feature.) |
LOL. Okay. |
|
+1 for @jmdavis comment about |
|
Also, if we add this, what is real world use case? |
|
I asked because range API is not universal. And maybe use cases you will find may require more specialised API. Note, that we do not need this for RNGs because it can be replaced with combination of This is good example where more generic != better. If one need a lazy by element, then he probably needs to be able to get a multiple bits at once. In other hand, if one need a random access binary range, an It looks better for me understand use cases and, if we have them, add adaptors to existing one API instead of introducing a new large piece of code. |
|
|
|
Proof of concept. struct BitMap
{
size_t* ptr;
import core.bitop;
bool opIndex(size_t index) const
{
return bt(ptr, index) != 0;
}
void opIndexAssign(bool val, size_t index)
{
if(val)
bts(ptr, index);
else
btr(ptr, index);
}
}
import std.experimental.ndslice;
void main()
{
auto arr = new size_t[3];
auto sl = BitMap(arr.ptr).sliced(size_t.sizeof * 8 * arr.length);
sl[4] = true;
sl[100] = true;
sl.popFrontN(3);
assert(sl[1]);
assert(sl[97]);
} |
1684929 to
c3e620e
Compare
std/range/package.d
Outdated
| { | ||
| elemIndex = 0; | ||
| } | ||
| size_t elemIndex = n / bitsNum + 1; |
There was a problem hiding this comment.
Do you think that the following construct would be faster?
elemIndex = n >> (bitsNum.bsf + 1) + 1;
|
This is still a WIP |
andralex
left a comment
There was a problem hiding this comment.
Let's drop the length cache, plus a few nits
std/range/package.d
Outdated
|
|
||
| static if (hasLength!R) | ||
| { | ||
| ulong len; |
There was a problem hiding this comment.
Let's delete this field and compute it on the fly from the source's length and the mask(s). It looks like more work but it's simpler and may actually be faster.
| } | ||
| } | ||
|
|
||
| bool empty() |
There was a problem hiding this comment.
Add a ddoc comment before this
std/range/package.d
Outdated
| bool front() | ||
| { | ||
| assert(!empty()); | ||
|
|
There was a problem hiding this comment.
no need for all this whitespace - just say what you mean and be done
std/range/package.d
Outdated
| parent.popFront; | ||
| maskPos = bitsNum; | ||
| } | ||
|
|
There was a problem hiding this comment.
too much whitespace - in Phobos we don't use whitespace around each compound statement
std/range/package.d
Outdated
|
|
||
| static if (hasLength!R) | ||
| { | ||
| ulong length() const |
std/range/package.d
Outdated
| static if (hasLength!R) | ||
| { | ||
| assert(n < len, | ||
| __PRETTY_FUNCTION__ ~ ": Index out of bounds"); |
std/range/package.d
Outdated
| By truncating n with maskPos bits we have skipped the remaining | ||
| maskPos bits in parent[0], so we need to add 1 to elemIndex. | ||
| */ | ||
| size_t elemIndex = n / bitsNum + 1; |
std/range/package.d
Outdated
| { | ||
| size_t sliceLen = end - start; | ||
| size_t startElemIndex; | ||
| size_t endElemIndex; |
There was a problem hiding this comment.
move down closest to initialization point
std/range/package.d
Outdated
| } | ||
|
|
||
| // Get the slice to be returned from the parent | ||
| R resultParent = (parent.save)[startElemIndex .. endElemIndex + 1]; |
There was a problem hiding this comment.
no need for save when indexing or slicing
std/range/package.d
Outdated
|
|
||
| typeof(return) result; | ||
|
|
||
| result.parent = resultParent; |
|
With this change, I have:
|
5f498cc to
5faa96d
Compare
| R = an input range to iterate over | ||
|
|
||
| Returns: | ||
| At minimum, an input range. If `R` was a forward, bidirectional or random |
There was a problem hiding this comment.
How about:
A
Bitwiseinput range with propagated forward, bidirectional and random access capabilities
| { | ||
| static if (hasLength!R) | ||
| { | ||
| return length() == 0; |
There was a problem hiding this comment.
prevalent style is to omit () on no-argument calls
| { | ||
| bool back() | ||
| { | ||
| assert(!empty()); |
There was a problem hiding this comment.
this function is not covered (please install the Codecov Chrome Extension and you'll see uncovered lines in red on this page)
|
|
||
| void popBack() | ||
| { | ||
| ++backMaskPos; |
| } | ||
|
|
||
| /** | ||
| Assignes `flag` to the `n`th bit within the range |
|
|
||
| Bitwise!R opSlice() | ||
| { | ||
| return this; |
| */ | ||
| static if (hasLength!R) | ||
| { | ||
| assert(n < length(), __PRETTY_FUNCTION__ ~ ": Index out of bounds"); |
There was a problem hiding this comment.
While __PRETTY_FUNCTION__ is nice, I think there was some consensus to just use plain/old asserts for range asserts.
| // Test opIndex and opSlice | ||
| /// | ||
| @system unittest | ||
| { |
There was a problem hiding this comment.
/// will render this as example on the public docs and we don't want to shock the users by exposing all intrinsic tests in the example.
Could you please add a nice example with one type for the public docs?
| assert(bw.empty()); | ||
| static if (isForwardRange!T) | ||
| { | ||
| assert(bw2.empty()); |
There was a problem hiding this comment.
ditto about not using parenthesis with no args
| Bitwise adapter over integral type ranges. Consumes the range elements bit by bit. | ||
|
|
||
| Params: | ||
| R = an input range to iterate over |
There was a problem hiding this comment.
It's probably better to be a bit more precise here -> `integral input range"
| R = an input range to iterate over | ||
|
|
||
| Returns: | ||
| At minimum, an input range. If `R` was a forward, bidirectional or random |
There was a problem hiding this comment.
How about:
A
Bitwiseinput range with propagated forward, bidirectional and random access capabilities
| assert(bw[1] == true); | ||
|
|
||
| auto bw2 = bw[0 .. $ - 5]; | ||
| auto bw3 = bw2[]; |
There was a problem hiding this comment.
unittest blocks in D are cheap and help the reader to show that there's a new context.
Also it has happened quite often that one accidentally has introduced errors by mistyping the variable end number e, g. 4 vs 5.
| } | ||
|
|
||
| // Test all range types over all integral types | ||
| /// |
There was a problem hiding this comment.
This is an internal test - so it shouldn't be exposed to the user. Rather provide small, simple "snippets" that can be quickly grasped (e.g. five lines).
| static if (isBidirectionalRange!T) | ||
| { | ||
| auto bw3 = bw.save; | ||
| auto bw4 = bw.save; |
There was a problem hiding this comment.
Cryptic naming like bw3, bw4 make it very hard to read your code :/
How about bwTestBack?
|
|
||
| // Test all range types over all integral types | ||
| /// | ||
| @safe unittest |
There was a problem hiding this comment.
with the enforce removed this should be possible as pure nothrow @safe
| bool front() | ||
| { | ||
| assert(!empty()); | ||
| return (parent.front & mask(maskPos)) != 0; |
There was a problem hiding this comment.
Calculating the mask every time seems to be an unnecessary expensive operation. What is your rationale for storing the position and not the mask directly?
(front is expected to be called a lot more often thanopSlice and opIndex`)
|
Auto-merge toggled on |
@andralex did you see @9il's discussion about adding more code bloat? http://forum.dlang.org/post/vnhmrfjjennoytgomkfs@forum.dlang.org |
Yes, but we deprecated ndslice |
|
Btw did anyone knew about |
|
@wilzbach yah, it's fairly specialized (for the GC) though. |
|
It's unfortunate that Ilya's point wasn't picked up. |
Add a generic bitwise adapter to std.range