-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce Transmogrifying from A to B #124
Conversation
field!(is_admin, true), | ||
field!(name, hlist![field!(is_admin, true)]), | ||
] | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because a lot of work is put into making identity transforms work, I'd like to see tests for:
- Identity transform with labeled fields
type Source = Hlist![
Field<name, Hlist![
Field<inner, f32>,
Field<is_admin, bool>,
]>,
Field<age, i32>,
Field<is_admin, bool>];
type Target = Source;
- Non-identity transform where a labelled field gets an identity transform
type Source = Hlist![
Field<name, Hlist![
Field<inner, f32>,
Field<is_admin, bool>,
]>,
Field<age, i32>,
Field<is_admin, bool>];
type Source = Hlist![
Field<age, i32>,
Field<name, Hlist![
Field<inner, f32>,
Field<is_admin, bool>,
]>];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like GH ate my comment; I gave these a go but they currently don't compile, and I suspect it's due to ambiguous identity impl
s for the inner hlists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, pretty sure at this point that the clash happens due to ambiguity of the inner fields that happen to be both identical and transmogrifiable (i.e. identical labelled HLists or a fields with the same name and same type that is also LabelledGeneric
); the two things are not mutually exclusive :(
Not entirely sure how to get around it at this point. Any ideas?
I'm not entirely sure I understand the purpose of fn test_transmogrify_simple_identity() {
let one: PluckedValue<i32> = PluckedValue(1);
let one_again: i32 = one.transmogrify();
assert_eq!(one_again, 1);
} I can't put my finger on exactly why I feel this way... but it feels to me like there are demons lurking just beneath the surface here, waiting for their chance to jump out and cause type inference errors. |
@ExpHP Thanks for the quick review :) IIRC I had to do use it in order to get make it so that the following 3 wouldn't clash with the non-trivial case where we needed to pluck things out. /// Implementation of Transmogrifier for when the target is empty and the Source is non-empty
impl<SourceHead, SourceTail> Transmogrifier<HNil, HNil> for HCons<SourceHead, SourceTail> {
#[inline(always)]
fn transmogrify(self) -> HNil {
HNil
}
} As I mentioned in the reply to your comment, the straight labelled-HList version doesn't compile at this point (though I think at sometime during my experimentation, I had something like that working), but the following, with
It would be really awesome if you could check out this branch and kick the tires so to speak and play around with it locally to see if you could figure out where I went wrong here and see if we can't get the straight-labelled HList version working (again) too; getting it working to this point for the |
Adding a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't looked over the wider semantic type inference implications here and I'll leave that to ExpHP; I have some nits and some unrelated thoughts however which came to me while reading this. I would generally go over the punctuation and backticks in the PR.
Rem, | ||
> CoproductSubsetter<Coproduct<THead, TTail>, HCons<NHead, NTail>> for Choices | ||
impl<Choices, THead, TTail, NHead, NTail, Rem> | ||
CoproductSubsetter<Coproduct<THead, TTail>, HCons<NHead, NTail>> for Choices |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice rustfmt improvements!
#[deprecated( | ||
since = "0.1.30", | ||
note = "Please use len() or static_len() instead." | ||
)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to this PR... Should we make this go poof in the next breaking release at some point? If so let's create a tracking issue for it. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we most definitely should ! #125
core/src/labelled.rs
Outdated
@@ -65,10 +64,95 @@ | |||
//! let s_user: ShortUser = frunk::transform_from(n_user); // done | |||
//! # } | |||
//! ``` | |||
//! | |||
//! If you have the need to transform types that are similarly-shaped recursively, then | |||
//! use the Transmogrifier trait |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
End of sentence punctuation missing.
core/src/labelled.rs
Outdated
//! /// ExternalUser, taking care of subfield conversions as well. | ||
//! let external_user: ExternalUser = internal_user.transmogrify(); | ||
//! | ||
//! println!("{:?}", external_user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd remove this line as it is redundant for explanation / testing purposes.
core/src/labelled.rs
Outdated
} | ||
} | ||
|
||
/// Implementation of Transmogrifier for when the target is empty and the Source is non-empty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Punctuation + backticks.
core/src/labelled.rs
Outdated
} | ||
} | ||
|
||
/// Implementation of Transmogrifier for when the target is an HList, and the Source is a Plucked HList |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Punctuation + backticks.
core/src/labelled.rs
Outdated
} | ||
} | ||
|
||
/// For the case where we need to do work in order to transmogrify one type into another |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Punctuation.
TargetTail, | ||
PluckSourceHeadNameIndex, | ||
TransMogSourceHeadValueIndices, | ||
TransMogTailIndices, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh dear lord =P
fn test_transmogrify_hcons_sculpting_super_simple() { | ||
type Source = Hlist![Field<name, &'static str>, Field<age, i32>, Field<is_admin, bool>]; | ||
type Target = Hlist![Field<age, i32>]; | ||
let hcons: Source = hlist!(field!(name, "joe"), field!(age, 3), field!(is_admin, true)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should add a macro:
record! { // bikeshed
name: "joe",
age: 3,
is_admin: true,
}
Possibly also:
Record! { // bikeshed
name: &'static str,
age: i32,
is_admin: bool,
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea ! Filed #126
@ExpHP so, I managed to get rid of the |
@Centril Thanks for the thorough review ! Went through and touched up the docs with punctuations and back ticks as you suggested :)
4e5a8bb
to
560c191
Compare
extracted from `InternalUser` do not implement `LabelledGeneric`, not does the values exptression extracted from `ExternalUser`: For some reason, the compiler does not seem to know to go one chunk at a time, solving for `&str: Transmogrify`, without having the whole thing transmogrify ``` error[E0277]: the trait bound `frunk::HCons<&str, frunk::HCons<ExternalCredentials, frunk::HCons<&str, frunk::HNil>>>: frunk::LabelledGeneric` is not satisfied --> examples/labelled.rs:76:50 | 76 | let external_user: ExternalUser = inner_user.transmogrify(); | ^^^^^^^^^^^^ the trait `frunk::LabelledGeneric` is not implemented for `frunk::HCons<&str, frunk::HCons<ExternalCredentials, frunk::HCons<&str, frunk::HNil>>>` | = note: required because of the requirements on the impl of `frunk::labelled::Transmogrifier<frunk::HCons<&str, frunk::HCons<ExternalCredentials, frunk::HCons<&str, frunk::HNil>>>, frunk::indices::There<_>, frunk::indices::There<frunk::HCons<frunk::indices::There<frunk::indices::Here>, frunk::HCons<frunk::indices::There<frunk::indices::Here>, frunk::HCons<frunk::indices::Here, frunk::HNil>>>>>` for `frunk::HCons<&str, frunk::HCons<InternalCredentials, frunk::HCons<&str, frunk::HNil>>>` = note: required because of the requirements on the impl of `frunk::labelled::Transmogrifier<ExternalUser<'_>, frunk::indices::There<frunk::indices::There<_>>, frunk::indices::There<frunk::HCons<frunk::indices::There<frunk::indices::Here>, frunk::HCons<frunk::indices::There<frunk::indices::Here>, frunk::HCons<frunk::indices::Here, frunk::HNil>>>>>` for `InternalUser<'_>` error[E0277]: the trait bound `frunk::HCons<&str, frunk::HCons<InternalCredentials, frunk::HCons<&str, frunk::HNil>>>: frunk::LabelledGeneric` is not satisfied --> examples/labelled.rs:76:50 | 76 | let external_user: ExternalUser = inner_user.transmogrify(); | ^^^^^^^^^^^^ the trait `frunk::LabelledGeneric` is not implemented for `frunk::HCons<&str, frunk::HCons<InternalCredentials, frunk::HCons<&str, frunk::HNil>>>` | = note: required because of the requirements on the impl of `frunk::labelled::Transmogrifier<frunk::HCons<&str, frunk::HCons<ExternalCredentials, frunk::HCons<&str, frunk::HNil>>>, frunk::indices::There<_>, frunk::indices::There<frunk::HCons<frunk::indices::There<frunk::indices::Here>, frunk::HCons<frunk::indices::There<frunk::indices::Here>, frunk::HCons<frunk::indices::Here, frunk::HNil>>>>>` for `frunk::HCons<&str, frunk::HCons<InternalCredentials, frunk::HCons<&str, frunk::HNil>>>` = note: required because of the requirements on the impl of `frunk::labelled::Transmogrifier<ExternalUser<'_>, frunk::indices::There<frunk::indices::There<_>>, frunk::indices::There<frunk::HCons<frunk::indices::There<frunk::indices::Here>, frunk::HCons<frunk::indices::There<frunk::indices::Here>, frunk::HCons<frunk::indices::Here, frunk::HNil>>>>>` for `InternalUser<'_>` ```
4d18860
to
c6b8b1f
Compare
Managed to get rid of PluckedValue
; the other tests proposed still don't work as explained, but I think we can improve on it later better than not having this at all? Open for another review :)
So, unfortunately, it doesn't really surprise me that one of those tests didn't work, and I'm pretty sure it's impossible to fix. The fundamental problem is that the only feasible way to let primitive and std types like Well, I shouldn't say that's the only feasible way. It's clear from the Haskell package's README how they handle it:
which is a pretty serious limitation. IMO, it is too easy to see the man behind the curtain, and it's too easy to break working impls by removing a field from the source type or adding one to the target type. |
Absolutely insane idea. Make of this what you will. #[derive(FlatLabelledGeneric)]
struct Field {
apples: u32,
oranges: u32,
}
#[derive(FlatLabelledGeneric)]
struct Thing {
number: u32,
#[frunk(flatten)]
field: Field,
string: String,
}
// <Thing as FlatLabelledGeneric>::Repr will be
//
// Hlist![
// field!("number", u32),
// field!("field.apples", u32),
// field!("field.oranges", u32),
// field!("string", String),
// ]
//
// where field!("string literal", ...) is understood to construct the type-level
// string corresponding to that literal
//
// the fields added through #[frunk(flatten)] are produced without
// knowledge of that type's definition, with the help of this trait:
/// Prefixes every field name in an HList of Fields with a fixed string.
trait PrefixFieldLabels<PrefixStr> {
type Prefixed;
fn prefix_field_labels(self) -> Self::Prefixed;
} With that, the implementation of let repr = FlatLabelledGeneric::into(self);
let mogged = repr.sculpt();
FlatLabelledGeneric::from(mogged) and correctly handles all fields, at the cost of the user needing to maintain the |
core/src/labelled.rs
Outdated
fn transmogrify(self) -> Target; | ||
} | ||
|
||
/// For the case where we don't need to do any transmogrifying at all because the source |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided to look at the new doc page for frunk_core::labelled
and it's pretty intimidating due to things like this. I wonder if this and other related index types should be moved to indices
?
The headline should also mention that it is an index type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. Moved :)
* Add examples to the readme * Add examples to the main lib doc
That's a really cool idea :D I'd probably need a bunch more time to explore it and build something that works 😄 I think one challenge there might be finding a way to unflatten the keys back out; super interesting though and I'm keen to explore in a follow up attempt to improve this ! |
Gonna merge this in for now since it feels mostly Good Enough and I don't want Perfect to be the enemy of Good. ; I think the overall traits + type-param signatures will probably remain more-or-less the same and we can always re-iterate on the implementation later. |
This PR adds the ability to do "transmogrifying" (inspired by this Haskell repo and this Scala SO answer) to Frunk.
What is "transmogrifying"? In this context, it means to convert some data of type A into data of type B, in a typesafe, recursive way, as long as A and B are "similarly-shaped". In other words, as long as B's fields and their subfields are subsets of A's fields and their respective subfields, then A can be turned into B.
As usual, the goal with Frunk is to do this:
unsafe
I think the compiler should be able to compile away most, if not all, of the performance penalties this imposes, much like it does for non-recursive "sculpting", but benchmarks will follow in another PR.
In any case, here is an example: