I did some more thinking and came up with the following:
// Machinery for type level if->0-then-else
// It is a bit unfortunate that we need all this boilerplate.
struct Nat<const N: usize>(PhantomData<[(); N]>);
trait IfSuccFn<S, Z> { type Out; }
// If Succ(N) => S
impl<const N: usize, S, Z> IfSuccFn<S, Z> for Nat<{N + 1}> { type Out = S; }
// If 0 => Z
impl<S, Z> IfSuccFn<S, Z> for Nat<{0}> { type Out = Z; }
// if N > 0 then S, else Z.
type IfSucc<const N: usize, S, Z> = <Nat<N> as IfSucc<S, Z>>::Out;
trait CSIterator<const N: usize> {
type Item;
type Next: CSIterator<{ N.saturating_sub(1) }, Item = Self::Item>;
fn next(self) -> (IfSucc<N, Self::Item, ()>, Self::Next);
}
fn foreach<Iter: CSIterator<N>, F: FnMut(Iter::Item), const N>(iter: Iter, fun: F) {
if N > 0 {
let (item, next) = iter.next();
fun(item);
foreach(next);
}
}
The main benefit of this encoding (assuming it actually type checks) is that we are not involving Option
at all.
EDIT: Improving readability of the encoding a bit:
// Machinery for type level if-then-else
struct Test<const B: bool>(PhantomData<[(); B as usize]>);
trait IfFn<S, Z> { type Out; }
impl<S, Z> IfFn<S, Z> for Test<{true }> { type Out = S; }
impl<S, Z> IfFn<S, Z> for Test<{false}> { type Out = Z; }
// if B then S, else Z.
type If<const B: bool, S, Z> = <Test<B> as IfFn<S, Z>>::Out;
trait CSIterator<const N: usize> {
type Item;
type Next: CSIterator<{ N.saturating_sub(1) }, Item = Self::Item>;
fn next(self) -> (If<{N == 0}, (), Self::Item>, Self::Next);
}
Ideally, you would really want to write:
trait CSIterator<const N: usize> {
type Item;
type Next: CSIterator<{ N.saturating_sub(1) }, Item = Self::Item>;
fn next(self) -> (if N == 0 { () } else { Self::Item }, Self::Next);
}
and that’s it.