chain_link

Crates.iochain_link
lib.rschain_link
version0.1.3
created_at2025-06-05 05:33:02.67397+00
updated_at2025-06-07 08:47:54.409945+00
descriptionMicro library with a util for chaining operations on a single type.
homepage
repository
max_upload_size
id1701093
size13,641
(IceKontroI)

documentation

README

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.
Commit count: 0

cargo fmt