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

Transmogrification fails if common field implements `LabelledGeneric` #143

Open bossmc opened 5 years ago

bossmc commented 5 years ago

A basic (working) example of transmogrification might look like:

use frunk_core::labelled::Transmogrifier;

#[derive(Debug, frunk_derives::LabelledGeneric)]
struct Foo {
    pub bar: Bar,
}

#[derive(Debug, frunk_derives::LabelledGeneric)]
struct Foo2 {
    pub bar: Bar2,
}

#[derive(Debug, frunk_derives::LabelledGeneric)]
struct Bar {
    pub index: i64,
}

#[derive(Debug, frunk_derives::LabelledGeneric)]
struct Bar2 {
    pub index: i64,
}

fn main() {
    let foo = Foo {
        bar: Bar { index: 4 }
    };
    let foo: Foo2 = foo.transmogrify();
    println!("{:?}", foo);
}

This compiles and works just fine but, if you change the definition of Foo2 to be[1]:

#[derive(Debug, frunk_derives::LabelledGeneric)]
struct Foo2 {
    pub bar: Bar,
}

Then the compile fails with the error:

error[E0282]: type annotations needed
  --> src/main.rs:27:25
   |
27 |     let foo: Foo2 = foo.transmogrify();
   |                         ^^^^^^^^^^^^ cannot infer type for `TransMogSourceHeadValueIndices`

Strangely, if Bar does not implement LabelledGeneric then the code compiles and works as expected. (Have you accidentally tricked the compiler into implementing specialization on stable?!)

This makes it hard to mix types in structs, since no two LabelledGeneric fields can ever be common between two LabelledGeneric structs.

[1](Note that this means that Foo and Foo2 are identical, though this need not be the case in general, as other fields may be present and differ up to transmogrifiability without affecting the result)

bossmc commented 5 years ago

Ah, I've worked out why this is failing, the compiler has hit the decision of whether to use the T -> T implementation or the T -> Repr -> T implementation and it can't decide which to use 😞.

Not sure what can be done here (especially if one wanted to move between these two Foo's and a third type that didn't contain an exact Bar...).

ExpHP commented 5 years ago

Yep, this issue was known since before the feature was added, and to be honest is something I regard as a crippling limitation. I really don't know what can be done about it in the confines of rust's trait system.

lloydmeta commented 5 years ago

Indeed, this was known and is noted in the Transmogrification section as well.

It's a fairly annoying limitation, true, but I decided to add it because (1) it's still got some use and (2) when specialisation is done we can hit the ground running :)

bossmc commented 5 years ago

Ah, yes, I missed that warning text. Two follow on questions:

1) Do you need more than specialisation? I think you need the "lattice impl" extension (maybe that's going to be part of specialisation when it lands?) since you have overlapping but non-containing impls:

* The two types are the same type
* The two types implement `LabelledGeneric` (with convertibility bounds)

2) Is there a way today to "steer" the type-inference machine? I'm thinking something like:

    <foo as Transmogrifier<Bar, (_, _ ,_ Identity, _, _)>>::transmogrify()

Except the types in the index field are incredibly hard to describe...  Maybe some extra macros to create the steering for the user?
lloydmeta commented 5 years ago

Ah, yes, I missed that warning text. Two follow on questions:

  1. Do you need more than specialisation? I think you need the "lattice impl" extension (maybe that's going to be part of specialisation when it lands?) since you have overlapping but non-containing impls:

    • The two types are the same type
    • The two types implement LabelledGeneric (with convertibility bounds)

I'll have to admit that I don't have an up-to-date view of what specialisation will bring in Rust, so you could well be right :D. In Scala, something like this works:

@ {
  trait Foo[A] {
    def go(o: A): Unit
  }

  object Foo {
    implicit val intFoo: Foo[Int] = new Foo[Int] {
      def go(o: Int): Unit = println(s"int foo [$o]")
    }

    implicit def allFoo[A]: Foo[A] = new Foo[A] {
      def go(o: A): Unit = println(s"all foo [$o]")
    }
  }
  }
defined trait Foo
defined object Foo

@ def fooIt[A: Foo](o: A) = implicitly[Foo[A]].go(o)
defined function fooIt

@ fooIt(3)
int foo [3]

@ fooIt("hello")
all foo [hello]
  1. Is there a way today to "steer" the type-inference machine? I'm thinking something like:
    <foo as Transmogrifier<Bar, (_, _ ,_ Identity, _, _)>>::transmogrify()

    Except the types in the index field are incredibly hard to describe... Maybe some extra macros to create the steering for the user?

Yeah, I'm pretty sure that it is possible to do something like that to steer the inference (at the very least, I did this during at least one of the implementations); but yeah, as you said, without something like a procedural macro it would be hard to do right. Definitely worth exploring though !

ExpHP commented 5 years ago

I lack confidence that specialization will help.

In the current form of the trait, specialization is ineffectual. Index is a type parameter, and will invariably be different for the two conflicting impls (the Self -> LabelledGeneric -> Self index must encode information about how to sculpt the fields, while the Self -> Self impl must be unitlike). Hence, the impls technically do not overlap.

This means Index must become an associated type. For that to work, it must also become an associated type of the hlist traits. This leaves us to ask a much simpler question:

And... well... I dunno! I really don't understand how specialization works or how much of it is even implemented, to be honest. I gave it a try, sprinking default all over the place, but whatever I do rust still simply complains that there are overlapping impls of Plucker<_> for HCons<_, _>.