lloydmeta / frunk

Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.
https://beachape.com/frunk/
MIT License
1.29k stars 58 forks source link

to_cons for HList #123

Closed Rua closed 6 years ago

Rua commented 6 years ago

When a function receives a HList trait object as a parameter, it's not possible to cast it back to a HCons. Consequently it's not possible to call the HCons-specific methods on it. There should be a method that, given a HList, can give you an Option<HCons<H, T>> or similar (with None used when the HList is actually HNil).

ExpHP commented 6 years ago

The H and T types would have to be specified somehow. What kind of use case do you have in mind?

(P.S. be careful, the term "trait object" is generally understood to refer to dynamic polymorphism! I would say "type parameter")

Centril commented 6 years ago

Hmm... how do you get a trait object like &HList or Box<HList> given pub trait HList: Sized { .. }?

ExpHP commented 6 years ago

(I'm going to continue assuming the author wasn't talking about trait objects :slightly_smiling_face: )

note: I see mainly two interpretations here, which are:


pub trait TryIntoHCons {
    type Head;
    type Tail: TryIntoHCons;
    fn try_into_hcons(self) -> Option<HCons<Self::Head, Self::Tail>>;
}

impl TryIntoHCons for HNil {
    type Head = !;
    type Tail = HNil;
    fn try_into_hcons(self) -> Option<HCons<!, HNil>> { None }
}

impl<H, T: TryIntoHCons> TryIntoHCons for HCons<H, T> {
    type Head = H;
    type Tail = T;
    fn try_into_hcons(self) -> Option<HCons<H, T>> { Some(self) }
}

which could possibly be used to make some functions on HLists less boiler-platey compared to the status quo (which is that you often need to write to write a trait instead of a function if you want to do structural induction on HCons vs HNil). I'm not sure, though; I wonder if the compiler will have difficulty proving that the bounds on such function are well-founded.

edit: ahh, lookitthat, it does work:

fn len<List: TryIntoHCons>(list: List) -> usize {
    match list.try_into_hcons() {
        None => 0,
        Some(HCons(_, tail)) => 1 + len(tail)
    }
}

fn main() {
    println!("{}", len(HCons((), HCons(1, HNil))));  // 2
}

The other possible interpretation is:

pub trait TryIntoHCons<H, T> {
    fn try_into_hcons(self) -> Option<HCons<H, T>>;
}

impl<H, T> TryIntoHCons<H, T> for HCons<H, T> {
    fn try_into_hcons(self) -> Option<HCons<H, T>> { Some(self) }
}

impl<H, T> TryIntoHCons<H, T> for HNil {
    fn try_into_hcons(self) -> Option<HCons<H, T>> { None }
}

But I don't see what the use cases would be for this in comparison to e.g. pluck.

lloydmeta commented 6 years ago

Consequently it's not possible to call the HCons-specific methods on it

This part got me thinking about what specific methods there are on HCons, and I think this is it

https://github.com/lloydmeta/frunk/blob/4905b28cafbf77d7ef793df9bb132f9c2389eae8/core/src/hlist.rs#L175-L192

https://github.com/lloydmeta/frunk/blob/4905b28cafbf77d7ef793df9bb132f9c2389eae8/core/src/hlist.rs#L163-L166

2 accessors (head, tail), and a pop. Just thinking aloud; would it perhaps make sense to Option-returning (for HNil) functions on Hlist to satisfy the same usecases as you would if a TryIntHCons ?

ExpHP commented 6 years ago

There is also get, pluck, and into_tuple2. However:

ExpHP commented 6 years ago

@lloydmeta if we had pop, head, and tail methods on HList, where would the Head and Tail types come from?

lloydmeta commented 6 years ago

@lloydmeta if we had pop, head, and tail methods on HList, where would the Head and Tail types come from?

Yeah, I clearly didn't think this through enough 😆 They could probably be associated types; maybe for HNil; they would be ! and they would need to return Nones. Again, still going off-the-cuff; might not actually work.

Rua commented 6 years ago

My use case is a function that is part of a trait. This function should receive a HList as an argument, and then call "get" on it to retrieve an item of a particular type, which then gets passed to another function.

trait Foo {
    fn call(&self, &HList, Vec<&str>);
}

struct Bar<T>
{
    func: Box<Fn(&T, Vec<&str>) + 'static>
}

impl<T> Foo for Bar<T> {
    fn call(&self, list: &HList, args: Vec<&str>) {
        (self.func)(list.get::<T, _>(), args);
    }
}

This code does not currently work because HList has no "get" method. However, if I try to rewrite it so that the "call" function receives HCons<T, Tail>, it doesn't work either, because the T of the list shadows the T of the struct. With HCons<Head, Tail>, I get "the trait frunk::hlist::Selector<T, _> is not implemented for Tail". So I'm not sure what the proper solution is in this case.

ExpHP commented 6 years ago

Oh, you are using a trait object! As @Centril explained, this could never work anyways, because HList: Sized.

error[E0038]: the trait `frunk_core::hlist::HList` cannot be made into an object
 --> src/main.rs:3:1
  |
3 | fn func(list: &HList) {}
  | ^^^^^^^^^^^^^^^^^^^^^ the trait `frunk_core::hlist::HList` cannot be made into an object
  |
  = note: the trait cannot require that `Self : Sized`
  = note: the trait cannot contain associated consts like `LEN`
ExpHP commented 6 years ago

I was going to suggest using &hlist::Selector<T, I> instead, but on further thought that won't work too well because it only works for fixed I.

Some alternatives I might recommend:

Rua commented 6 years ago

I have a HashMap that stores Foos, so it can't have any type parameters or they will no longer fit together into one map.

The func closure doesn't need a HList, because it only needs a single object of type T. The purpose of the call function is to find that object among the list given to it, which is why it needs to call .get. Essentially what is going on is that the HashMap is storing a bunch of Bar<T> with different T's (as trait objects), and each of them has to select its own matching T out of a HList.

Rua commented 6 years ago

I've experimented some more and it seems that there is something going on with implementations of Selector. With the code

impl<T> Foo for Bar<T> {
    fn call<Head, Tail>(&self, list: &HCons<Head, Tail>, args: Vec<&str>) {
        (self.func)(list.get::<T, _>(), args);
    }
}

I get the error "the trait frunk::hlist::Selector<T, _> is not implemented for Tail", as I mentioned before. So I thought, maybe the problem is that it can't figure this out for a generic type T. So I tried:

impl Foo for Bar<i32> {
    fn call<Head, Tail>(&self, list: &HCons<Head, Tail>, args: Vec<&str>) {
        (self.func)(list.get::<i32, _>(), args);
    }
}

so now with a concrete type i32 instead. However I get the same message: "the trait frunk::hlist::Selector<i32, _> is not implemented for Tail". When I add an additional requirement, then it works:

impl Foo for Bar<i32> {
    fn call<I, Head, Tail: Selector<i32, I>>(&self, list: &HCons<Head, Tail>, args: Vec<&str>) {
        (self.func)(list.get::<i32, _>(), args);
    }
}

However, this means that the type of item requested is now hardcoded into the trait Foo which is exactly what I wanted to avoid.

ExpHP commented 6 years ago

However, this means that the type of item requested is now hardcoded into the trait Foo which is exactly what I wanted to avoid.

Well, it's going to have to be, at least to some extent. What I mean is that, at the very least it will be necessary to encode the full set of possible types T somewhere.

For instance, here's something I tried:

/// `hlist::Selector` with the index type moved to the method
pub trait Has<T> {
    fn get_any_index<I>(&self) -> &T
    where Self: hlist::Selector<T, I>;
}

impl<Head, Tail, T> Has<T> for HCons<Head, Tail> {
    fn get_any_index<I>(&self) -> &T
    where Self: hlist::Selector<T, I>,
    { self.get() }
}

// The various "T" types you need
pub struct A(i32);
pub struct B(String);
pub struct C {
    bax: String,
    buzz: (f64, f64),
}
// ...

pub trait BigHList
    : Has<A>
    + Has<B>
    + Has<C>
{ }

trait Foo {
    // (unfortunately this doesn't compile because Has is not object-safe)
    fn call(&self, list: &BigHList, Vec<&str>);
}

Notice how BigHList must define all supported T types. This is absolutely unavoidable; otherwise rust could not know what T types to use when populating the vtable.


...unfortunately, however, even with that limitation, the above idea does not work, because a generic method like fn get_any_index<I>(&self) is not object-safe (rust doesn't know what types I to populate the vtable with). So I'm still not sure how your goal could be accomplished in any fashion.

Rua commented 6 years ago

Predefining the allowed T types is livable, at least, though of course it makes the code less generic. A possible alternative I thought of is to pass a struct containing all the different object types to call, rather than a HList. Instead, Frunk's generics library is used inside call to convert it into a HList, from which the appropriate type can then be extracted to pass on to the callback. Would this approach be feasible?

Edit: Come to think of it, could I not just statically define the exact type of list that call receives using the Hlist! type macro?

ExpHP commented 6 years ago

Ah, so it's always the same HList type? Yeah, that should make things much easier.

Yes, you can do something along those lines. Here's a suggestion:

#[macro_use]
extern crate frunk_core;

/// `hlist::Selector` with no mention of index types.
/// (it is only implemented on MyHList)
pub trait FooGet<T> {
    fn foo_get(&self) -> &T;
}

// The various "T" types you need
pub struct A(i32);
pub struct B(String);
pub struct C {
    pub bax: String,
    pub buzz: (f64, f64),
}
// ...

pub type MyHList = Hlist![A, B, C];

// Generate FooGet impls
macro_rules! impl_foo_get {
    ($($T:ty),+) => {
        $(
            impl FooGet<$T> for MyHList {
                fn foo_get(&self) -> &$T {
                    self.get()
                }
            }
        )+
    }
}
impl_foo_get!{ A, B, C }

pub trait Foo {
    fn call(&self, list: &MyHList, strs: Vec<&str>);
}

pub struct Bar<T> {
    func: Box<Fn(&T, Vec<&str>) + 'static>
}

// use FooGet as the bound for Bar
impl<T> Foo for Bar<T>
where MyHList: FooGet<T>
{
    fn call(&self, list: &MyHList, args: Vec<&str>) {
        (self.func)(list.foo_get(), args);
    }
}
ExpHP commented 6 years ago

Closing because I don't think there's anything actionable here for frunk.

Let us know if you still have trouble.