-
-
Notifications
You must be signed in to change notification settings - Fork 746
Range adaptor: cache #1364
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
Range adaptor: cache #1364
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,8 @@ $(MYREF equal) $(MYREF levenshteinDistance) $(MYREF levenshteinDistanceAndPath) | |
$(MYREF max) $(MYREF min) $(MYREF mismatch) $(MYREF clamp) $(MYREF | ||
predSwitch)) | ||
) | ||
$(TR $(TDNW Iteration) $(TD $(MYREF filter) $(MYREF filterBidirectional) | ||
$(TR $(TDNW Iteration) $(TD $(MYREF cache) $(MYREF cacheBidirectional) | ||
$(MYREF filter) $(MYREF filterBidirectional) | ||
$(MYREF group) $(MYREF joiner) $(MYREF map) $(MYREF reduce) $(MYREF | ||
splitter) $(MYREF sum) $(MYREF uniq) ) | ||
) | ||
|
@@ -742,6 +743,257 @@ unittest | |
auto m = immutable(S).init.repeat().map!"a".save; | ||
} | ||
|
||
/++ | ||
$(D cache) eagerly evaluates $(D front) of $(D range) | ||
on each construction or call to $(D popFront), | ||
to store the result in a cache. | ||
The result is then directly returned when $(D front) is called, | ||
rather than re-evaluated. | ||
|
||
This can be a useful function to place in a chain, after functions | ||
that have expensive evaluation, as a lazy alternative to $(XREF array,array). | ||
In particular, it can be placed after a call to $(D map), or before a call | ||
to $(D filter). | ||
|
||
$(D cache) may provide bidirectional iteration if needed, but since | ||
this comes at an increased cost, it must be explicitly requested via the | ||
call to $(D cacheBidirectional). Furthermore, a bidirectional cache will | ||
evaluate the "center" element twice, when there is only one element left in | ||
the range. | ||
|
||
$(D cache) does not provide random access primitives, | ||
as $(D cache) would be unable to cache the random accesses. | ||
If $(D Range) provides slicing primitives, | ||
then $(D cache) will provide the same slicing primitives, | ||
but $(D hasSlicing!Cache) will not yield true (as the $(XREF range,hasSlicing) | ||
trait also checks for random access). | ||
+/ | ||
auto cache(Range)(Range range) | ||
if (isInputRange!Range) | ||
{ | ||
return Cache!(Range, false)(range); | ||
} | ||
|
||
/// ditto | ||
auto cacheBidirectional(Range)(Range range) | ||
if (isBidirectionalRange!Range) | ||
{ | ||
return Cache!(Range, true)(range); | ||
} | ||
|
||
/// | ||
unittest | ||
{ | ||
import std.stdio, std.range; | ||
import std.typecons : tuple; | ||
|
||
ulong counter = 0; | ||
double fun(int x) | ||
{ | ||
++counter; | ||
// http://en.wikipedia.org/wiki/Quartic_function | ||
return ( (x + 4.0) * (x + 1.0) * (x - 1.0) * (x - 3.0) ) / 14.0 + 0.5; | ||
} | ||
// Without cache, with array (greedy) | ||
auto result1 = iota(-4, 5).map!(a =>tuple(a, fun(a)))() | ||
.filter!"a[1]<0"() | ||
.map!"a[0]"() | ||
.array(); | ||
|
||
// the values of x that have a negative y are: | ||
assert(equal(result1, [-3, -2, 2])); | ||
|
||
// Check how many times fun was evaluated. | ||
// As many times as the number of items in both source and result. | ||
assert(counter == iota(-4, 5).length + result1.length); | ||
|
||
counter = 0; | ||
// Without array, with cache (lazy) | ||
auto result2 = iota(-4, 5).map!(a =>tuple(a, fun(a)))() | ||
.cache() | ||
.filter!"a[1]<0"() | ||
.map!"a[0]"(); | ||
|
||
// the values of x that have a negative y are: | ||
assert(equal(result2, [-3, -2, 2])); | ||
|
||
// Check how many times fun was evaluated. | ||
// Only as many times as the number of items in source. | ||
assert(counter == iota(-4, 5).length); | ||
} | ||
|
||
unittest | ||
{ | ||
auto a = [1, 2, 3, 4]; | ||
assert(equal(a.map!"(a - 1)*a"().cache(), [ 0, 2, 6, 12])); | ||
assert(equal(a.map!"(a - 1)*a"().cacheBidirectional().retro(), [12, 6, 2, 0])); | ||
auto r1 = [1, 2, 3, 4].cache() [1 .. $]; | ||
auto r2 = [1, 2, 3, 4].cacheBidirectional()[1 .. $]; | ||
assert(equal(r1, [2, 3, 4])); | ||
assert(equal(r2, [2, 3, 4])); | ||
} | ||
|
||
unittest | ||
{ | ||
//immutable test | ||
static struct S | ||
{ | ||
int i; | ||
this(int i) | ||
{ | ||
//this.i = i; | ||
} | ||
} | ||
immutable(S)[] s = [S(1), S(2), S(3)]; | ||
assert(equal(s.cache(), s)); | ||
assert(equal(s.cacheBidirectional(), s)); | ||
} | ||
|
||
@safe pure nothrow unittest | ||
{ | ||
//safety etc | ||
auto a = [1, 2, 3, 4]; | ||
assert(equal(a.cache(), a)); | ||
assert(equal(a.cacheBidirectional(), a)); | ||
} | ||
|
||
unittest | ||
{ | ||
char[][] stringbufs = ["hello".dup, "world".dup]; | ||
auto strings = stringbufs.map!((a)=>a.idup)().cache(); | ||
assert(strings.front is strings.front); | ||
} | ||
|
||
unittest | ||
{ | ||
auto c = [1, 2, 3].cycle().cache(); | ||
c = c[1 .. $]; | ||
auto d = c[0 .. 1]; | ||
} | ||
|
||
private struct Cache(R, bool bidir) | ||
{ | ||
import core.exception : RangeError; | ||
|
||
private | ||
{ | ||
alias E = ElementType!R; | ||
alias UE = Unqual!E; | ||
|
||
R source; | ||
|
||
static if (bidir) alias CacheTypes = TypeTuple!(UE, UE); | ||
else alias CacheTypes = TypeTuple!UE; | ||
CacheTypes caches; | ||
|
||
static assert(isAssignable!(UE, E) && is(UE : E), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be moved to the template constraints? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be hard to express in a short and "grokkable" way for the end user. Also, I still believe that template constraints is for "logical disambiguation", and not "internal validation". This is the overload that works on input ranges. From there, the static assert is only designed as a way to cleanly express a compile error. We do not want allow someone else to write a "cache" that could also operates on input ranges, with completly different semantics, just because the element type has a particular trait. This would require an explicit disambiguation (even if one of them wouldn't compile anyways, it is close enough logically to be ambigous). |
||
algoFormat("Cannot instantiate range with %s because %s elements are not assignable to %s.", R.stringof, E.stringof, UE.stringof)); | ||
} | ||
|
||
this(R range) | ||
{ | ||
source = range; | ||
if (!range.empty) | ||
{ | ||
caches[0] = range.front; | ||
static if (bidir) | ||
caches[1] = range.back; | ||
} | ||
} | ||
|
||
static if (isInfinite!R) | ||
enum empty = false; | ||
else | ||
bool empty() @property | ||
{ | ||
return source.empty; | ||
} | ||
|
||
static if (hasLength!R) auto length() @property | ||
{ | ||
return source.length; | ||
} | ||
|
||
E front() @property | ||
{ | ||
version(assert) if (empty) throw new RangeError(); | ||
return caches[0]; | ||
} | ||
static if (bidir) E back() @property | ||
{ | ||
version(assert) if (empty) throw new RangeError(); | ||
return caches[1]; | ||
} | ||
|
||
void popFront() | ||
{ | ||
version(assert) if (empty) throw new RangeError(); | ||
source.popFront(); | ||
if (!source.empty) | ||
caches[0] = source.front; | ||
else | ||
caches = CacheTypes.init; | ||
} | ||
static if (bidir) void popBack() | ||
{ | ||
version(assert) if (empty) throw new RangeError(); | ||
source.popBack(); | ||
if (!source.empty) | ||
caches[1] = source.back; | ||
else | ||
caches = CacheTypes.init; | ||
} | ||
|
||
static if (isForwardRange!R) | ||
{ | ||
private this(R source, ref CacheTypes caches) | ||
{ | ||
this.source = source; | ||
this.caches = caches; | ||
} | ||
typeof(this) save() @property | ||
{ | ||
return typeof(this)(source.save, caches); | ||
} | ||
} | ||
|
||
static if (hasSlicing!R) | ||
{ | ||
enum hasEndSlicing = is(typeof(source[size_t.max .. $])); | ||
|
||
static if (hasEndSlicing) | ||
{ | ||
private static struct DollarToken{} | ||
enum opDollar = DollarToken.init; | ||
|
||
auto opSlice(size_t low, DollarToken) | ||
{ | ||
return typeof(this)(source[low .. $]); | ||
} | ||
} | ||
|
||
static if (!isInfinite!R) | ||
{ | ||
typeof(this) opSlice(size_t low, size_t high) | ||
{ | ||
return typeof(this)(source[low .. high]); | ||
} | ||
} | ||
else static if (hasEndSlicing) | ||
{ | ||
auto opSlice(size_t low, size_t high) | ||
in | ||
{ | ||
assert(low <= high); | ||
} | ||
body | ||
{ | ||
return this[low .. $].take(high - low); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/++ | ||
Implements the homonym function (also known as $(D accumulate), $(D | ||
compress), $(D inject), or $(D foldl)) present in various programming | ||
|
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 wonder if @DmitryOlshansky's sliding window range idea could be used here and, more broadly, if there is actually some overlap between
cache()
and his concept.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 is, which is something I find intriguing after looking at this again. Given that @andralex wasn't objecting adding a couple of primitives which is a good sign.
I'm going to (hopefully soon) do a short writeup on a buffer range design and new i/o.
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 like the idea of a sliding window range. It could even be applicable to the prospective
std.io
.