eldesh/iter-zig
A basic iterator library written in Zig
iter-zig is a lazy style generic iterator combinator library written in Zig. Where the iterator means that the types enumrating all element of a set of values.
This library is developped with:
zig build
To performs unit tests of iter-zig:
zig build test
An example program using iter-zig is provided. The program can be performed with:
zig build example
Source code of this program is src/main.zig.
To generate documentations:
zig build doc
A html documents would be generated under the ./docs directory.
Iterator is a generic concept that objects that enumrating a set of values. Especially, in this library, Iterator is a value of a kind of types that satisfies follow constraints.
The constraints are:
Self typeItem typenext method takes *Self and returns ?Item.Where the Self type specifies the type itself (equals to @This()), the Item type represents the type of elements returns from the iterator, and the next method returns a 'next' value of the container.
If the next value is not exists, 'null' must be returned.
The order of occurence of values are implementation defined.
However, all values must occur exactly once before 'null' is returned.
The type constraints required as an Iterator is able to be checked by isIterator function statically.
comptime assert(isIterator(SliceIter(u32)));
comptime assert(!isIterator(u32));
When you implement a new type to be an iterator, it must ensure that isIterator returns true.
iter-zig provides several basic iterators that wraps standard containers.
For example, an iterator on a slice is able to be used as follows:
var arr = [_]u32{ 1, 2, 3 };
var iter = SliceIter(u32).new(arr[0..]);
try expectEqual(@as(u32, 1), iter.next().?.*);
try expectEqual(@as(u32, 2), iter.next().?.*);
try expectEqual(@as(u32, 3), iter.next().?.*);
try expectEqual(@as(?*u32, null), iter.next());
Further, Const variations are defined for each containers.
These iterators behaves as same to non-const variations except for returns const pointers.
var arr = [_]u32{ 1, 2, 3 };
var iter = SliceConstIter(u32).new(arr[0..]);
iter.next().?.* += 1; // error: cannot assign to constant
Note that, these iterators not own container values into it. The user must release the memory holding the container if necessary.
Iterator to container converters are defined for some std containers.
These converters consume the input iterator and build a container. The constructed container contains all the elements of the iterator and no other elements.
range creates a Range value such that it represents a range of numbers.
For example, range(0, 10) means that the numbers from 0 to 10, which is mathematics is denoted as [0,10).
In particular, Range instantiated with integral type to be iterator.
var rng = range(@as(u32, 0), 3);
try expectEqual(@as(u32, 0), rng.next().?);
try expectEqual(@as(u32, 1), rng.next().?);
try expectEqual(@as(u32, 2), rng.next().?);
try expectEqual(@as(?u32, null), rng.next());
Similarly, range_from creates an instance of RangeFrom to represents an endless sequence of numbers.
e.g. range_from(@as(u32, 0)) creates an endless sequence of natural numbers 0,1,2,....
var rng = range_from(@as(u32, 0));
try expectEqual(@as(u32, 0), rng.next().?);
try expectEqual(@as(u32, 1), rng.next().?);
try expectEqual(@as(u32, 2), rng.next().?);
..
When Range or RangeFrom is instantiated with a type of floating numbers,
it would not be an Iterator. It is just a range of values.
comptime assert(!concept.isIterator(range.Range(f64)));
comptime assert(!concept.isIterator(range.RangeFrom(f64)));
comptime assert( range.range(@as(f64, 2.0), 3.0).contains(2.5))
comptime assert(!range.range(@as(f64, 2.0), 3.0).contains(1.5))
All iterators defined in this library provide iterator functions below.
These functions are almost same to functions on Iterator trait of Rust except for experimental api.
Some functions above return an iterator from the iterator itself.
For that case, the functions are implemented in adaptor style.
For example, the map function returns a new iterator object Map rather than apply a function to each elements from the iterator.
var arr = [_]u32{ 1, 2, 3 };
var iter = SliceIter(u32).new(arr[0..]);
fn incr(x:u32) u32 { return x+1; }
// Calculations have not yet been performed.
var map: Map(SliceIter(u32), fn (u32) u32) = iter.map(incr);
try expectEqual(@as(u32, 2), map.next().?.*); // incr is performed
try expectEqual(@as(u32, 3), map.next().?.*); // incr is performed
try expectEqual(@as(u32, 4), map.next().?.*); // incr is performed
try expectEqual(@as(?*u32, null), map.next());
iter-zig allows library users to implement a new iterator type by their self.
Further, it is easy to implement all functions showed in Iterator Operators to your new iterator type using DeriveIterator.
For example, let's make an iterator Counter which counts from 1 to 5.
const Counter = struct {
pub const Self = @This();
pub const Item = u32;
count: u32,
pub fn new() Self { return .{ .count = 0 }; }
pub fn next(self: *Self) ?Item {
self.count += 1;
if (self.count < 6)
return self.count;
return null;
}
};
Now we can use it as an iterator.
comptime assert(isIterator(Counter));
var counter = Counter.new();
try expectEqual(@as(u32, 1), counter.next().?);
try expectEqual(@as(u32, 2), counter.next().?);
try expectEqual(@as(u32, 3), counter.next().?);
try expectEqual(@as(u32, 4), counter.next().?);
try expectEqual(@as(u32, 5), counter.next().?);
try expectEqual(@as(?u32, null), counter.next());
However, Counter not implement utility functions like map or count etc ...
To implement these functions, use DeriveIterator meta function like below.
const CounterExt = struct {
pub const Self = @This();
pub const Item = u32;
pub usingnamespace DeriveIterator(@This()); // Add
count: u32,
pub fn new() Self { return .{ .count = 0 }; }
pub fn next(self: *Self) ?Item {
self.count += 1;
if (self.count < 6)
return self.count;
return null;
}
};
In above code, CounterExt difference from Counter is only the DeriveIterator(@This()) line.
Now, you can use all functions showed in Iterator Operators.
fn incr(x:u32) u32 { return x+1; }
fn even(x:u32) bool { return x % 2 == 0; }
fn sum(st:*u32, v:u32) ?u32 {
st.* += v;
return st.*;
}
var counter = CounterExt.new();
var iter = counter
.map(incr) // 2,3,4,5,6
.filter(even) // 2,4,6
.scan(@as(u32, 0), sum); // 2,6,12
try expectEqual(@as(u32, 2), iter.next().?);
try expectEqual(@as(u32, 6), iter.next().?);
try expectEqual(@as(u32, 12), iter.next().?);
try expectEqual(@as(?u32, null), iter.next());
If you can implement some method efficiently rather than using next method, just implement that method (in CounterExt in the above).
DeriveIterator suppresses the generation of that function.
iter-zig adopts naming conventions for implementing iterators.
First, when defining a new iterator type, the type constructor must be named MakeT where type T is a name of the type.
And the constructor should take a Derive function like below.
pub fn MakeCounter(comptime Derive: fn (type) type) type {
return struct {
pub const Self = @This();
pub const Item = u32;
pub usingnamespace Derive(@This());
count: u32,
pub fn next(self: *Self) ?Item {
...
}
};
}
This allows users to switch the function used for deriving.
Second, a type constructor should be named T.
And the constructor should forward DeriveIterator to MakeT.
pub fn Counter() type {
return MakeCounter(DeriveIterator);
}