Thread overview
Adapting foreign iterators to D ranges
Apr 22
Chloé
Apr 23
cc
Apr 25
cc
April 22
Assume a third-party API of the following signature:

    T* next(I iter);

which advances an iterator of sorts and returns the next element, or null when iteration is done. No other information about the state of the iterator is available.

I wish to adapt this interface to a forward range for use with foreach and Phobos' range utilities. This amounts to implementing empty, front, and popFront, in terms of next and some state. But there is a choice to be made regarding the first call to next.

One could call next during range construction:

    struct Range
    {
        private I iter;
        private T* current;
        this(I iter) { this.iter = iter; current = next(iter); }
        bool empty() const => current is null;
        inout(T)* front() inout => current;
        void popFront() { current = next(iter); }
    }

Or do not call it until the first call to empty:

    struct Range
    {
        private bool initialized;
        private I iter;
        private T* current;
        this(I iter) { this.iter = iter; }
        bool empty()
        {
            if (!initialized) {
                current = next(iter);
                initialized = true;
            }
            return current is null;
        }
        inout(T)* front() inout => current;
        void popFront() { current = next(iter); }
    }

The first implementation has the advantage is being simpler and empty being const, but has the downside that next is called even if the range ends up not being used. Is either approach used consistently across the D ecosystem?
April 22
On Monday, 22 April 2024 at 11:36:43 UTC, Chloé wrote:
> The first implementation has the advantage is being simpler and empty being const, but has the downside that next is called even if the range ends up not being used. Is either approach used consistently across the D ecosystem?

You can also place initialization logic inside front, then empty could become const.

I don't think there is a preffered way of initializing such ranges, so imho consider what's best for your use case.

April 23

On Monday, 22 April 2024 at 11:36:43 UTC, Chloé wrote:

>

The first implementation has the advantage is being simpler and empty being const, but has the downside that next is called even if the range ends up not being used. Is either approach used consistently across the D ecosystem?

I always go for the simplest approach. So that means, pre-fill in the constructor.

Yes, the downside is, if you don't use it, then the iterator has moved, but the range hasn't. But returning to the iterator after using the range is always a dicey proposition anyway.

The huge benefit is that all the functions become simple and straightforward.

But there is no "right" approach. And using composition, you may be able to achieve all approaches with wrappers. Phobos does various things depending on what people thought was good at the time. It sometimes causes some very unexpected behavior.

I recommend always using the same approach for the same library, that way your users know what to expect!

-Steve

April 23

On Monday, 22 April 2024 at 11:36:43 UTC, Chloé wrote:

>

I wish to adapt this interface to a forward range for use with foreach and Phobos' range utilities. This amounts to implementing empty, front, and popFront, in terms of next and some state. But there is a choice to be made regarding the first call to next.

Just to offer an alternative solution (since it sometimes gets overlooked), there is also the opApply approach. You don't get full forward range status, and checking whether it's empty essentially requires doing something like std.algorithm walkLength, but if all you need is basic iteration, it can be a simpler solution:

struct Range {
	private I iter;
	this(I iter) { this.iter = iter; }
	int opApply(scope int delegate(T* t) dg) {
		while (auto current = next(iter)) {
			if (auto r = dg(current))
				return r;
		}
		return 0;
	}
}
void main() {
	I someIter; // = ...
	auto range = Range(someIter);
	foreach (const t; range) {
		writeln(*t);
	}
}
April 24

On Tuesday, 23 April 2024 at 06:02:18 UTC, cc wrote:

>

Just to offer an alternative solution (since it sometimes gets overlooked), there is also the opApply approach. You don't get full forward range status, and checking whether it's empty essentially requires doing something like std.algorithm walkLength, but if all you need is basic iteration, it can be a simpler solution:

Yes, opApply() works! You just need to use do while() instead of while() because it skips the first item.

struct Node
{
  int item;
  Node* next;
}

class List
{
  Node* root, iter;

  this(int item = 0)
  {
    iter = new Node(item, null);
    root = iter;
  }

  List dup()
  {
    auto backup = new List();
    backup.root = root;
    backup.iter = iter;

    return backup;
  }

  void insertFront(T)(T item)
  {
    (*iter).next = new Node(item, null);
    this.Next;
  }

  bool empty() const => iter is null;
  auto front() inout => iter;
  auto popFront()    => iter = this.Next;
  auto getItem()     => iter.item;
  auto rewind()      => iter = root;
}

auto Next(List list) => list.iter = list.iter.next;
auto gaussian(T)(T n)=> (n * n + n) / 2;

void main()
{
  import std.stdio;
  enum LIMIT = 10;

  auto list = new List(1);
  foreach(t; 2 .. LIMIT + 1)
  {
    list.insertFront(t);
  }

  auto tmp = list.dup;
  list.rewind();

  size_t sum;
  do sum += list.getItem; while(list.Next);
  assert(gaussian(LIMIT) == sum);

  sum.writeln; // 55

  auto next = LIMIT + 1;
  tmp.insertFront(next);
  tmp.rewind();
  sum = 0;

  foreach(t; tmp) sum += t.item;
  assert(gaussian(LIMIT) + next == sum);

  sum.writeln; // 66
  tmp.rewind();

  auto range = Range(tmp);

  foreach(r; range) r.item.write(" ");
  writeln; //  2 3 4 5 6 7 8 9 10 11
  // ? (1) --^
}

struct Range
{
  private List iter;

  int opApply(scope int delegate(Node* t) dg)
  {
    while(auto current = iter.Next)
    {
      if (auto r = dg(current))
        return r;
    }
    return 0;
  }
}

SDB@79

April 25

On Wednesday, 24 April 2024 at 05:08:25 UTC, Salih Dincer wrote:

>

Yes, opApply() works! You just need to use do while() instead of while() because it skips the first item.

It depends on the type of structure being consumed, if it provides "next" as a direct pointer then yeah you would need to consume the item first before iterating to the next in line. However some APIs provide an opaque iterator type where you call a "next" method to get the first element, IIRC Lua does something like this.

September 30

On Thursday, 25 April 2024 at 03:18:36 UTC, cc wrote:

>

On Wednesday, 24 April 2024 at 05:08:25 UTC, Salih Dincer wrote:

>

Yes, opApply() works! You just need to use do while() instead of while() because it skips the first item.

It depends on the type of structure being consumed, if it provides "next" as a direct pointer then yeah you would need to consume the item first before iterating to the next in line. However some APIs provide an opaque iterator type where you call a "next" method to get the first element, IIRC Lua does something like this.

I've noticed a strange behavior in the Range structure that consumes the List class! If we use foreach(), we should take a backup as we're used to, or use the rewind() function as I did below. These days, I've started to delve deeper into the opApply() feature. I wanted to fix this mistake I made in the past.

struct Range
{
  private List list;

  int opApply(scope int delegate(Node* t) dg)
  {
    while(auto current = list.Next)
    {
      if (auto r = dg(current))
        return r;
    }
    list.rewind();
    return 0;
  }
}

Also, due to the nature of linked lists, we cannot print the number 1 added at the beginning with foreach(). Therefore, when creating the List class, it may be wise to create it without giving any parameters and start adding elements from 1. You might also be interested in this topic about opApply:

https://forum.dlang.org/thread/jxzqsxasierzokgcykjh@forum.dlang.org

SDB@79