output ranges: by ref or by value?

Andrei Alexandrescu SeeWebsiteForEmail at erdani.org
Sat Jan 2 21:49:08 PST 2010


Steven Schveighoffer wrote:
> My theory is, given this list of ranges, if you pair them with an 
> algorithm that requires save capability, you wouldn't want to use that 
> algorithm on it anyways (kinda like the consume example).

Why gratuitously limit the design? You're asking to replace this:

R save() { return this; }

with:

enum thisIsAForwardRange = true;

Is there a reason? The former leaves in flexibility. The latter doesn't, 
for no good reason.

>> Struct ranges won't work with a container hierarchy. If you define a 
>> container hierarchy (classes etc.) you'll also need a range hierarchy. 
>> Otherwise defining the inheritance relationship is impossible. Consider:
>>
>> abstract class Container(E) { // most general container
>>      @property bool empty();
>>      bool add(E element);
>>      E removeAny();
>>      InputRange!E opSlice();
>> }
>>
>> That's what I'd expect any container worth its salt to implement: (a) 
>> test for empty; (b) add an element to the container; (c) remove some 
>> element from the container; (d) get a range that spans the entire 
>> container. (Probably removeAll() and a range-positioned remove() would 
>> make sense, too.)
> 
> The range interface is compile-time only, there is no need to define it 
> in the container interface.  opSlice does not need to be part of the 
> interface, even if it is defined on all the container types.
> 
> opApply makes for a much better interface-friendly iteration anyways.

I want container users to be able to save iteration state and also to 
open up containers to std.algorithm and other goodies, so I'm shying 
away from opApply.

> BTW, the primitives in dcollections are:
> 
> clear(); // clear all elements
> remove(V v); // remove an element

Search and remove? That's an odd primitive. Why wouldn't you offer an 
interface for iteration that allows an algorithm for search, and a 
primitive for positioned removal?

> contains(V v); // returns whether an element is contained in the colleciton

I don't think this belongs to primitives. It's O(n) for many containers 
and again it's a generic algorithm, not a member.

> length(); // the length of the collection

That's not a primitive either. std.algorithm.walkLength is. For me, all 
sorts of red flags and alarm buzzers go off when primitives are 
guaranteed that can't be implemented efficiently but by a subset of 
containers. You can't discount complexity as a implementation detail.

> dup(); // duplicate the collection
> opApply(int delegate(ref V v) dg); // iterate the collection
> opApply(int delegate(ref bool doPurge, ref V v) dg); // purge the 
> collection
> 
> That means it covers only empty in your list of must-have functions (via 
> length() == 0).

How do you implement length() for a singly-linked list? Is empty() going 
to take O(n)?

> Add is not a primitive because the Map collections 
> shouldn't assign some random key to the new element.  removeAny is 
> defined only on sets and multisets, but I'm not sure that it couldn't be 
> moved to Collection, in fact, I'll probably do that.

add is a primitive that takes Tuple!(K, V) as the element type.

> Note that it's missing begin and end which are defined on every single 
> container type (i.e. the equivalent of the all-elements range).  This is 
> because those primitives return a struct that is different for every 
> container type.

So you can't write container-independent iteration code unless you use 
opApply, in which case composition becomes tenuous.

> It also surpasses opSlice via opApply, since all an input range can do 
> anyways is iterate.  In fact, opApply is more powerful because you can 
> change elements and remove elements (via purging).  Plus it's more 
> efficient than a range-via-interface.

An input range is a subset of other (more powerful) ranges. It's also 
much more flexible. I agree that calling range primitives via interfaces 
can become an efficiency liability.

>>> I see a range as being useful for iteration or algorithms, but not 
>>> for general use.  A great example is AAs.  Would you say that an AA 
>>> *is* a range or should *provide* a range?  If it is a range, does 
>>> that mean you remove elements as you call popFront?  Does that make 
>>> any sense?  If it doesn't, then what happens if you add elements 
>>> through another alias to that AA?
>>
>> An AA provides several ranges - among which byKey and byValue.
> 
> I misunderstood your statement "[a container hierarchy] does need range 
> interfaces."  I thought you meant containers themselves need to 
> implement the range interface, I see now that isn't the case, so my bad.

Yah, they'd offer it as a result of opSlice(). Covariant return types 
will ensure there's no virtual call when you know what container you 
operate on.

Above all: the primitive set for a container must be a small set of 
functions that (a) can be implemented by all containers within 
reasonable efficiency bounds, and (b) are container-specific, not 
generic. IMHO any container design that defines a search(Element) as a 
primitive is misguided. Searching is not a container primitive - it's an 
algorithm. Only more specialized containers (e.g. trees, hashes etc.) 
can afford to define search(Element) as a primitive. Linear search is a 
generic algorithm that works the same for everyone. It does not belong 
as a method of any container.


Andrei



More information about the Digitalmars-d mailing list