Skip to content

Commit

Permalink
Range adaptor: cache
Browse files Browse the repository at this point in the history
As discussed in this thread:
http://forum.dlang.org/thread/ovbjcmogezbvsxrwfcol@forum.dlang.org

This provides a range adaptor that cache the result of another range.

Meant as a lazy alternative to array. Not much else to say...?

Documentation might suck.
  • Loading branch information
monarchdodra committed Sep 16, 2014
1 parent 575b4ae commit ebcc18e
Showing 1 changed file with 253 additions and 1 deletion.
254 changes: 253 additions & 1 deletion std/algorithm.d
Expand Up @@ -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) )
)
Expand Down Expand Up @@ -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),
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
Expand Down

0 comments on commit ebcc18e

Please sign in to comment.