View mode: basic / threaded / horizontal-split · Log in · Help
December 31, 2012
How to correctly handle immutable ranges
Here's a few facts:

(assume R is any range type)

1. All immutable(R) cannot be iterated using popFront (obviously).

2. Most immutable(R) cannot be iterated using foreach. The 
exception is immutable(T[]), which can.

3. In theory, immutable(R) could be iterated if it had immutable 
opIndex and length. However, to return a subrange you also need 
immutable opSlice. foreach does not use this.

4. When passed into a function, top-level immutable is removed 
for arrays and pointers, but no other type (can someone tell me 
why?).


Now, suppose I want to write a simple function that looks for a 
subrange in a range of ranges. Here's what I would probably write 
(ignoring custom predicates, optimisations etc.)

RoR findSubrange(RoR, R)(RoR ror, R r)
    if (isInputRange!RoR &&
        isInputRange!(ElementType!RoR) &&
        isInputRange!R)
{
	for (; !ror.empty; ror.popFront())
		if (equal(ror.front, r))
			break;
	return ror;
}

This function has a few problems:

A. If you pass an immutable array for ror, the top-level 
immutable will be stripped, giving you a mutable ror. Great. 
However, only the top level immutable is stripped. If the 
subranges are immutable then they will stay immutable and the 
second line of the template constraint will fail.

B. If you change the second line to 
isInputRange!(Unqual!(ElementType!RoR)) then you will correctly 
accept immutable arrays, but you also end up accepting ranges of 
other immutable ranges, which in general are not iterable, so the 
call to equal will fail.

C. If you change the second line to isIterable!(ElementType!RoR) 
then it will fail when the element type has opApply. Also, equal 
only requires isInputRange, so whether or not it fails depends on 
whether equal uses popFront or not (it doesn't for arrays, but it 
could...)

D. If RoR is any sort of wrapper around arrays (e.g. 
map!"a"(ror)) then it will fail because top level immutable is 
not stripped. In theory, you could iterate the map over array by 
using opIndex, length, and return a slice, but that would require 
a lot of duplication of code.


My questions are:

1. First, why is top-level immutable stripped for arrays and 
pointers but not other types?
2. How should you handle the special case of ranges of immutable 
arrays in a consistent manner?
3. How should you handle the alternate iteration method with 
opIndex/length/opSlice?

Cheers,
Peter
December 31, 2012
Re: How to correctly handle immutable ranges
On Monday, 31 December 2012 at 13:31:44 UTC, Peter Alexander 
wrote:
> My questions are:
>
> 1. First, why is top-level immutable stripped for arrays and 
> pointers but not other types?
> 2. How should you handle the special case of ranges of 
> immutable arrays in a consistent manner?
> 3. How should you handle the alternate iteration method with 
> opIndex/length/opSlice?
>
> Cheers,
> Peter

1. I'd say: Just because it can: If you pass in "immutable(T[])", 
then it is 100% safe to declare that that copy is a 
"immutable(T)[]". Ditto for pointers.

The same can't be said about ranges: a "immutable(Range!T)" might 
not safely copyable as a "Range!(immutable(T))"

2. I *might* start by providing "??? opSlice(void) const", which, 
when present, would allow extracting a "Range!(immutable(T))" 
from a "immutable(T)[]". It wouldn't help *much*, so no generic 
code could exploit this (as it wouldn't be required).

3. No idea.

--------
That said, some might question  the entire concept of an 
"immutable range". a "range of immutables" makes sense, the other 
way around: less so (IMO).

As for the special case of RoIR: If the top level R exposes its 
elements by ref, then it would be just plain wrong to strip the 
immutability of the underlying ranges. You could only legally 
iterate on *copies* of the subranges, but not the ranges 
themselves. This just brings us back to the problem of top level 
Immutable Ranges: IMO, RoIR is not really a special case compared 
to simple IR (outside of the slice of slice case, I guess).
December 31, 2012
Re: How to correctly handle immutable ranges
On Monday, 31 December 2012 at 14:24:53 UTC, monarch_dodra wrote:
> The same can't be said about ranges: a "immutable(Range!T)" 
> might not safely copyable as a "Range!(immutable(T))"

Hmm, are you sure? Can you give an example? Surely if Range is a 
struct and everything is copied then it can all change from 
mutable to immutable (using a postblit where necessary).

Maybe we just need copy constructors for this to work around the 
const issue with postblit.


> 3. No idea.

There is one way you could work around it in a generic manner, 
but involves a bit of boilerplate:

auto iterable(R)(R r)
{
	struct Iterable
	{
		this(R range) { _range = range; }

		static if (isInputRange!R)
		{
			alias _range this;
			alias _range result;
		}
		else static if (isRandomAccess!R && hasLength!R && hasSlicing!R)
		{
			@property auto front() { return _range[_index]; }
			@property bool empty() const { return _index == _range.length; 
}
			void popFront() { ++_index; }
			@property auto save() { return this; }
			@property R result() { return _range[_index..$]; }
			private size_t _index = 0;
		}

		private R _range;
	}
	return Iterable(r);
}

You just have to use iterable everywhere instead of the range, 
and then use .result to get back to the original range type. e.g.

RoR findSubrange(RoR, R)(RoR ror, R r)
{
        auto _ror = iterable(ror);
        auto _r = iterable(r);
	for (; !_ror.empty; _ror.popFront())
		if (equal(iterable(_ror.front), _r))
			break;
	return _ror.result;
}

Warning: code is totally untested, but should give the gist of 
the idea.


> That said, some might question  the entire concept of an 
> "immutable range". a "range of immutables" makes sense, the 
> other way around: less so (IMO).

I kind of agree, but unfortunately immutable arrays are iterable, 
and arrays are the most common range, so we really need to 
support them.
December 31, 2012
Re: How to correctly handle immutable ranges
On Monday, 31 December 2012 at 14:58:03 UTC, Peter Alexander 
wrote:
> On Monday, 31 December 2012 at 14:24:53 UTC, monarch_dodra 
> wrote:
>> The same can't be said about ranges: a "immutable(Range!T)" 
>> might not safely copyable as a "Range!(immutable(T))"
>
> Hmm, are you sure? Can you give an example?

Yes, any (well, most) reference type range will fail the 
conversion. For example:

import std.array;

//---
struct ShallowRange(T)
{
  T[]* payload;

  T front() const {return (*payload).front;}
  void popFront(){(*payload).popFront;}
  bool empty() const {return (*payload).empty;}
}

ShallowRange!T shallowRange(T)(ref T[] data)
{
  return ShallowRange!T(&data);
}

immutable(ShallowRange!T) immutableShallowRange(T)(ref 
immutable(T[]) data)
{
  return cast(immutable(ShallowRange!T)) 
ShallowRange!T(cast(T[]*)&data);
}

void main()
{
  immutable(int)[] slice  = [1, 2, 3];
  immutable(int[]) iSlice = [1, 2, 3];

  ShallowRange!(immutable(int)) range  = shallowRange(slice);
  immutable(ShallowRange!int)   iRange = 
immutableShallowRange(iSlice);
}
//---

Here, I have a slice of immutables (slice) and an immutable slice 
(iSlice), which I place in a range of immutables, and an 
immutable range (respectivelly).

If you cast my immutable range to a range of immutables, and then 
attempt to iterate over it, you are going to modify my immutable 
iSlice...

>  Surely if Range is  a struct...

I'm not well versed in how classes and immutability work 
(especially when discussing the immutability of the class object 
itself, vs the class + class reference).

But yeah, keep in mind a range can be implemented as a class.

> ... and everything is copied then it can all change from 
> mutable to immutable (using a postblit where necessary).
>
> Maybe we just need copy constructors for this to work around 
> the const issue with postblit.

I think this is a language wide issue (well, concept). A range 
really is no different from any other object, and you really 
can't know what will happen when you copy them.

As a general rule, even via CC, you can't assume the result of 
copying a "immutable(S!T)" (or "immutable(S!(immutable(T))") will 
be a "S!(immutable(T)".

--------
But back to the original problem, I just think one shouldn't be 
able to call a const object a range. A "Range" is mutable by 
nature. Any attempt to pass one to an algorithm should fail (IMO).

I've yet to see anybody try to manipulate const ranges anyways 
(which probably isn't a coincidence), though someone one prove me 
wrong/disagree.

We just have a really special case regarding const slices. Even 
then, the only reason you can "iterate them" is because the 
compiler is implicitly copying a mutable slice behind the scenes. 
The const slice itself, technically, really isn't iterable 
(AFAIK)...

I think the slices' behavior twists our view of what to expect 
from the rest of our ranges.
December 31, 2012
Re: How to correctly handle immutable ranges
On Monday, 31 December 2012 at 16:29:27 UTC, monarch_dodra wrote:
> But back to the original problem, I just think one shouldn't be 
> able to call a const object a range. A "Range" is mutable by 
> nature. Any attempt to pass one to an algorithm should fail 
> (IMO).

This will break a lot of existing code.

This currently works:

immutable int[] a = [1, 2, 3];
auto b = array(a);

std.array.array is one of the few functions that uses 
std.traits.isIterable, so it works for immutable slices.


> We just have a really special case regarding const slices. Even 
> then, the only reason you can "iterate them" is because the 
> compiler is implicitly copying a mutable slice behind the 
> scenes. The const slice itself, technically, really isn't 
> iterable (AFAIK)...

It could also iterate the indices:

immutable int[] a = [1, 2, 3];
foreach (i; 0..a.length)
    foo(a[i]);

Nothing illegal about that.

The specific issue I'm looking at is the joining of an immutable 
array of arrays. It should be allowed, because they can be 
iterated. In fact, if you just remove the template constraints 
from std.array.join then it works.
December 31, 2012
Re: How to correctly handle immutable ranges
On Monday, 31 December 2012 at 17:11:01 UTC, Peter Alexander 
wrote:
> On Monday, 31 December 2012 at 16:29:27 UTC, monarch_dodra 
> wrote:
>> But back to the original problem, I just think one shouldn't 
>> be able to call a const object a range. A "Range" is mutable 
>> by nature. Any attempt to pass one to an algorithm should fail 
>> (IMO).
>
> This will break a lot of existing code.
>
> This currently works:
>
> immutable int[] a = [1, 2, 3];
> auto b = array(a);
>
> std.array.array is one of the few functions that uses 
> std.traits.isIterable, so it works for immutable slices.

Yes, but technically, the passed object is "immutable(int)[]", 
which *is* a range, so that has nothing to do with isIterable.

But you do bring up a good point. All too often we settle for 
InputRange when we can have Iterable.

That said, it does come with its own problems, such as:
* Figuring out the iteration type?
* Iterate by ref, or by value?
* Difficult to handle "special first step" cases...

Last but not least, I was told by Andrei not too long ago (while 
working on reduce) that "opApply" objects where *not* supported 
by std.algorithm. I was even told to remove existing code that 
supported it.

>> We just have a really special case regarding const slices. 
>> Even then, the only reason you can "iterate them" is because 
>> the compiler is implicitly copying a mutable slice behind the 
>> scenes. The const slice itself, technically, really isn't 
>> iterable (AFAIK)...
>
> It could also iterate the indices:
>
> immutable int[] a = [1, 2, 3];
> foreach (i; 0..a.length)
>     foo(a[i]);
>
> Nothing illegal about that.
>
> The specific issue I'm looking at is the joining of an 
> immutable array of arrays. It should be allowed, because they 
> can be iterated. In fact, if you just remove the template 
> constraints from std.array.join then it works.

Well, the reason it works is not that they can be iterated, but 
that the constness of the subslices is stripped when passed to 
Appender.Put (which, btw, accepts InputRanges, but Iterables). 
Really, this is more luck than an "isIterable" thing.

--------
But you do bring up the issue of giving more love to iterables.

If you want to add support to joiner, you *may* have to start 
with adding support for it for appender though. I'm unsure: 
Appender doesn't accept Iterables, but the free standing put 
does, so it might be able to individually call 
Appender.put(element). Not sure... if it works... or is 
efficient...

In any case, I think it'd be wrong to special case support for 
Range of Immutable Array. At best, the type Immutable Array is 
iterable, but not a range. Trying to change that would (IMO) warp 
the language.
January 02, 2013
Re: How to correctly handle immutable ranges
On Monday, December 31, 2012 15:58:02 Peter Alexander wrote:
> On Monday, 31 December 2012 at 14:24:53 UTC, monarch_dodra wrote:
> > The same can't be said about ranges: a "immutable(Range!T)"
> > might not safely copyable as a "Range!(immutable(T))"
> 
> Hmm, are you sure? Can you give an example? Surely if Range is a
> struct and everything is copied then it can all change from
> mutable to immutable (using a postblit where necessary).

immutable(Range!T) and Range!(immutable(T)) are completely different types. 
They may come from the same template, but they're different instantiations of 
that template, and there's no guarantee that they have any relation to each 
other. In a pathalogical case, you could have something like

struct S(T)
{
static if(is(T == immutable))
{
double d;
int foo() { return 7; }
}
else
{
int i;
string s = "hello";
bool maybe;

double bar(int i) { return i * 1.1; }
void baz() { writeln(i); }
}
}

So, the compiler can't assume that immutable(Range!T) and Range!(immutable(T)) 
have any real relation with one another.

Ideally, that problem could be solved by declaring an overload of opSlice 
which returned a tail-const version of the range, but that poses difficulties 
due to circular dependencies when one template instantiation refers to 
another. The circular dependency problem might be solvable with a static if 
though. I'd have to play around with it more to figure out the details. It's a 
bit of a thorny problem, and I've had issues with it in the past.

But if a tail-const opSlice can be done, then if you need a tail-const version 
of a range, you can just slice the range to get it, which is what happens with 
arrays. It's just that with them, it's done automatically. If the opSlice stuff 
got sorted out well enough though, and it was generally accepted how to define 
a tail-const opSlice, then it might be reasonable for the compiler to use that 
where appropriate, giving ranges the same tail-const abilities as arrays.

- Jonathan M Davis
Top | Discussion index | About this forum | D home