Search code examples
rusttype-level-computation

How can I obtain the individual elements of a heterogeneous list without knowing their type?


Given an implementation of a heterogeneous list in rust (for example like the one from frunk), how can I obtain a reference to an element within the list without knowing the concrete type of the element? The length of the list is known statically, but cannot be hard-coded as numeric literal.

I have tried to obtain the individual elements by popping each element off the list sequentially (as shown in the code example) and by writing an indexing method that accepts a usize index as argument. Neither attempts even compile. The posted example is what I would like to do.

pub trait HList: Sized {
    const LEN: usize;

    type Head;
    type Tail: HList;

    fn push<E>(self, element: E) -> Element<E, Self> {
        Element { head: element, tail: self }
    }

    fn pop(self) -> Option<(Self::Head, Self::Tail)>;

    fn len(&self) -> usize {
        Self::LEN
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct Element<H, T> {
    pub head: H,
    pub tail: T,
}

impl<H, T: HList> HList for Element<H, T> {
    const LEN: usize = 1 + <T as HList>::LEN;

    type Head = H;
    type Tail = T;

    fn pop(self) -> Option<(H, T)> {
        Some((self.head, self.tail))
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct End;

impl HList for End {
    const LEN: usize = 0;

    type Head = End;
    type Tail = End;

    fn pop(self) -> Option<(Self::Head, Self::Tail)> {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pop_two_for() {
        let h = End
            .push(0usize)
            .push(String::from("Hello, World"));

        fn eval<H: HList>(l: H) {
            for _ in 0usize..H::LEN {
                let (e, l) = l.pop().unwrap();
                // Do work with `e`.
            }
        }

        eval(h);
    }
}

Solution

  • I have found a reasonable recursive solution, but it only works out if I can modify the HList trait based on the trait requirements of the work to be done. This is fine for my use case, even if it's not as generic as I would have liked it to be.

    use std::fmt::Debug;
    
    pub trait MyCustomTrait<'a>: Debug {}
    
    impl<'a> MyCustomTrait<'a> for () {}
    
    impl<'a> MyCustomTrait<'a> for usize {}
    
    impl<'a> MyCustomTrait<'a> for String {}
    
    pub trait HList: Sized {
        const LEN: usize;
    
        type Head: for<'a> MyCustomTrait<'a>;
        type Tail: HList;
    
        fn push<E>(self, element: E) -> Element<E, Self>
        where
            E: for<'a> MyCustomTrait<'a>,
        {
            Element::new(element, self)
        }
    
        fn head(&self) -> &Self::Head;
    
        fn tail(&self) -> &Self::Tail;
    
        fn len(&self) -> usize {
            Self::LEN
        }
    }
    
    #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
    pub struct Element<H, T>(H, T);
    
    impl<H, T: HList> Element<H, T> {
        pub fn new(head: H, tail: T) -> Self {
            Element(head, tail)
        }
    }
    
    impl<H, T> HList for Element<H, T>
    where
        H: for<'a> MyCustomTrait<'a>,
        T: HList,
    {
        const LEN: usize = 1 + <T as HList>::LEN;
    
        type Head = H;
        type Tail = T;
    
        fn head(&self) -> &Self::Head {
            &self.0
        }
    
        fn tail(&self) -> &Self::Tail {
            &self.1
        }
    }
    
    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
    pub struct End;
    
    impl HList for End {
        const LEN: usize = 0;
    
        type Head = ();
        type Tail = End;
    
        fn head(&self) -> &Self::Head {
            &()
        }
    
        fn tail(&self) -> &Self::Tail {
            &End
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn artbitrary_recursive() {
            let h = End
                .push(0usize)
                .push(String::from("Hello, World"));
    
            fn eval<H: HList>(list: &H) {
                if H::LEN > 0 {
                    let head = list.head();
                    // Do work here, like print the debug representation of `head`.
                    eprintln!("{:?}", head);
    
                    eval(list.tail());
                }
            }
    
            eval(&h);
            assert!(false);
        }
    }