#tuple #array #vector #bit-vector #push #iterator #little

ordes

A crate for treating arrays and tuples a little bit more like vectors

11 releases

0.3.4 Mar 28, 2023
0.3.3 Dec 30, 2022
0.3.2 Jun 19, 2022
0.2.4 Jul 3, 2021
0.1.0 Apr 13, 2021

#764 in Rust patterns

42 downloads per month

EUPL-1.2

26KB
555 lines

Ordes

A crate for treating arrays and tuples a little bit more like vectors.

At the moment, this crate provides .pop(), .rest(), .push(val), and .cons(val) methods through the OrdesPop, OrdesRest, OrdesPush, and OrdesCons traits respectively, and additionally provides the .split(), .concat(arr), .remove(), and .insert(val) methods through the OrdesSplit, OrdesConcat, OrdesRemove, and OrdesInsert traits respectively through the const_generics crate feature, which requires a nightly compiler and enables the incomplete generic_const_exprs feature. These last four traits are only available and implemented through this feature since each individual one would require a nonlinearly-scaling number of trait implementations, and I'm not sure anyone would like to put up with those compile times. For this same reason they're also only implemented on arrays.

This crate was born out of my stubborn desire to use iterators wherever possible instead of for loops, which makes packing data together a bit of a pain in the neck at times. For instance, consider an iterator producing all lowercase four letter "words":

('a'..='z')
    .flat_map(|l1| ('a'..='z').map(move |l2| (l1, l2)))
    .flat_map(|(l1, l2)| ('a'..='z').map(move |l3| (l1, l2, l3)))
    .flat_map(|(l1, l2, l3)| ('a'..='z').map(move |l4| (l1, l2, l3, l4)))
    .for_each(|(l1, l2, l3, l4)| println!("{}{}{}{}", l1, l2, l3, l4));

As we can see, this is both:

  1. Not particularly pleasant to write or look at
  2. Not something a sane person would do

This crate offers an alternative:

use ordes::OrdesPush;
('a'..='z')
    .flat_map(|l1| ('a'..='z').map(move |l2| (l1, l2)))
    .flat_map(|chars| ('a'..='z').map(move |l3| chars.push(l3)))
    .flat_map(|chars| ('a'..='z').map(move |l4| chars.push(l4)))
    .for_each(|(l1, l2, l3, l4)| println!("{}{}{}{}", l1, l2, l3, l4));

There's not any real magic going on here. .push(val) produces a value of a new type. In the first example, chars.push(l3), the input type is (char, char) and the output is (char, char, char). Similar story for chars.push(l4) - input of (char, char, char), output of (char, char, char, char). A nearly identical implementation can be made with arrays as well:

use ordes::OrdesPush;
('a'..='z')
    .flat_map(|l1| ('a'..='z').map(move |l2| [l1, l2]))
    .flat_map(|chars| ('a'..='z').map(move |l3| chars.push(l3)))
    .flat_map(|chars| ('a'..='z').map(move |l4| chars.push(l4)))
    .for_each(|[l1, l2, l3, l4]| println!("{}{}{}{}", l1, l2, l3, l4));

In this case, chars.push(l3) takes in [char; 2] and produces [char; 3], and chars.push(l4) takes in [char; 3] and produces [char; 4].

A silly use case that came up recently that prompted me to add two new traits - OrdesRest and OrdesCons<T> - is tagging a fixed-size array with an indicator byte before serialising that into a bytestream:

use std::net::IpAddr;
use ordes::OrdesCons;

fn ipaddr_bytestream(addr: IpAddr) -> impl Iterator<Item = u8> {
    let mut data = [0; 17];
    let len = match addr {
        IpAddr::V4(addr) => {
            data[0..5].copy_from_slice(&addr.octets().cons(4));
            5
        },
        IpAddr::V6(addr) => {
            data[0..17].copy_from_slice(&addr.octets().cons(6));
            17
        }
    };
    data.into_iter().take(len)
}

Without .cons(_), it would be necessary to include another line in each branch writing 4 or 6 to data[0]. Small improvement, but I'm petty enough to count it.

In this crate I implement OrdesPop, OrdesRest, OrdesPush<T>, and OrdesCons<T> for arrays and tuples, but with some caveats:

  • On stable, all traits are only implemented for array lengths up to 32 (by default, there's features for up to 1024).
  • On both nightly and stable, all traits are only implemented for tuples up to 32 types (by default, there's features for up to 1024).

All trait methods exposed by this crate consume self and produce a new type that's longer or shorter (or with an extra or dropping the last type in the case of tuples). I'm well aware this is a very limited use case, but it's useful to me, and hopefully a few other people.

Dependencies

~1.5MB
~33K SLoC