Adapting foreign iterators to D ranges
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?
Re: Adapting foreign iterators to D ranges
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.
Re: Adapting foreign iterators to D ranges
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
Re: Adapting foreign iterators to D ranges
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: ```d 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); } } ```