chain_link 0.1.3

Micro library with a util for chaining operations on a single type.
Documentation
  • Coverage
  • 0%
    0 out of 18 items documented0 out of 10 items with examples
  • Size
  • Source code size: 13.64 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.9 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 13s Average build duration of successful builds.
  • all releases: 18s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • IceKontroI

Overview

Small crate to help you link different impls together and then chain their executions. I'm not aware of any crate or rust feature that lets you iterate on a sequence of different implementations of the same trait, so I made one. This was inspired by a project of mine that required trait impls on each field of a struct. I needed to be able to execute some logic on each qualifying named field of a struct, where each field had a unique impl of some shared trait.

3rd party integration

For 3rd party crates that want to extend more specific functionality to its users can follow this pattern:

use chain_link::*;

pub struct NamesOf<T>(T);

pub trait Name<const N: usize>
where
    // doing this makes it impossible for the user to 
    // impl Length<Len = L<4>> then subsequently impl
    // Name<N> for any N >= 4, keeping it InRange
    Self: InRange<N, <Self as Length>::Len>
{
    const NAME: &'static str;
}

impl<T: Length> Length for NamesOf<T> {
    type Len = T::Len;
}

impl<const N: usize, T: Name<N>> Chain<N> for NamesOf<T>
where
    Self: InRange<N, Self::Len>
{
    type In<'a> = String;
    type Out<'a> = String;
        
    fn chain(input: Self::In<'_>) -> Self::Out<'_> {
        format!("{}, {}", input, T::NAME)
    }
}

Then your users can follow this pattern:

struct Beatles;

impl Name<0> for Beatles {
    const NAME: &'static str = "John Lennon";
}

impl Name<1> for Beatles {
    const NAME: &'static str = "Paul McCartney";
}

impl Name<2> for Beatles {
    const NAME: &'static str = "George Harrison";
}

impl Name<3> for Beatles {
    const NAME: &'static str = "Ringo Starr";
}
        
impl Length for Beatles {
    type Len = L<4>;
}

Which allows the following to be valid:

let header = "Names of the Beatles: ".to_owned();
let result = NamesOf::<Beatles>::cascade(header);
assert_eq!(
    result, 
    "Names of the Beatles: , John Lennon, Paul McCartney, George Harrison, Ringo Starr"
);

Pipeline

For custom iteration where no specific trait impl is needed, and a simple pipeline-like cascade is required, this pattern is quite serviceable:

struct Pipeline;

impl Chain<0> for Pipeline {
    type In<'a> = f32;
    type Out<'a> = i32;
        
    fn chain(input: Self::In<'_>) -> Self::Out<'_> {
        assert_eq!(input, -1.5);
        input as i32
    }
}

impl Chain<1> for Pipeline {
    type In<'a> = i32;
    type Out<'a> = u32;
        
    fn chain(input: Self::In<'_>) -> Self::Out<'_> {
        assert_eq!(input, -1);
        input as u32
    }
}

impl Chain<2> for Pipeline {
    type In<'a> = u32;
    type Out<'a> = String;
    fn chain(input: Self::In<'_>) -> Self::Out<'_> {
        assert_eq!(input, u32::MAX);
        let mut output = String::new();
        let mut n = input;
        while n >= 1_000 {
            output = format!(",{:03}{}", n % 1_000, output);
            n /= 1_000;
        }
        format!("{n}{}", output)
    }
}

impl Length for Pipeline {
    type Len = L<3>;
}
        
let input = -1.5;
let actual = Pipeline::cascade(input);
let expected = "4,294,967,295";
assert_eq!(actual, expected);

Limitations & Future Features

This is very much a WIP crate, with some limitations I'd like to work on

  • Due to limitations with rust's const generics, we're forced (I think) to set a hardcoded limit to supported valid lengths. Right now it's set at 16, and I noticed compiler slowdown at higher numbers. If rust supported constant math, I could use {N - 1} in the trait impl for Chain<N> and Link<N>. And if we could do conditional math like {N < M}, I could impl InRange<N> for T: Length<Len = L<M>>. This would remove the limitation, but I haven't found a way to do this in stable rust. This would also eliminate the need for seq_macro dependency, making it 100% native rust, which would be cool.
  • Due to a known blindspot in rust's compiler, I'm forced to complicate the impl for InRange<N, L<M>>, making the business logic hard to follow. Not a big issue, but would be nice for a cleanup. Removing L<M> in general would be nice, but it's necessary for fixing this blindspot in other parts of the code too.
  • It's possibly quite valuable to replace L<M> with Num<M> and use this type as a general-purpose compile-time constant indexing type for chains. However currently we're just working with numbered trait impls, so it's not clear exactly what type a user might want to receive as output to the index function.
  • A lot of logic revolves around requiring the Length trait. This produces a lot of boilerplate code, but also serves as a guardrail preventing users from implementing Chain where N >= the Length. This helps prevent a scenario where a user implements a chain of some initial length, then adds another link but forgets to increment the Length, resulting in no change in Cascade behavior.
  • Cascade can only ever execute for a chain of 0..Length, which is suitable for most common needs, but it would be a lot better if we could trigger it for any arbitrary valid range. Not sure of concrete use-cases but the API should be flexible enough to handle this use-case.
  • Portability is workable, but has limitations because 3rd party crates cannot impl Chain<N> for T: CustomTrait<N> because Chain is defined in an external crate and CustomTrait is defined locally. It's still possible to do, but adds a lot of boilerplate code which is suboptimal. See the Beatles example above for reference.
  • Structs are currently restricted to one Chain impl. Workable patterns exist, similar to the Beatles example above. Without adding a second generic param to Chain, we can only have one Chain impl, but it keeps business logic clean, so this is acceptable.
  • Often we don't care to define custom In/Out types per Chain impl, as is the case for uniform Cascades. A new Sequence trait could make usability much simpler, keeping In/Out the same. Also with uniform In/Out comes some increase in flexibility; things like the ability to iterate in reverse, or execute only a select few indexes of the sequence.