TaskLog: Improvement of IntoOwned
I'm back with lightingcss
again! But it's just an example I selected, and what I want to say is:
You need to repeat to learn how to think. Thinking is passing by, and there are so many that you shouldn't think you can use the same way next time just because you've used it once.
Let's see the PR for today.
This is super complex PR, of course. That's why it deserves the example of these blog posts. If a task is very complex, it's quite easy to analyze my approaches after finishing the work.
Problem
While trying to use the result of the previous work, I found that I forgot to add derive
s for IntoOwned
.
Oops.
But well, I'm very used to doing things like this. This should not be a big deal. So I just opened lightningcss
in vscode and added
#[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))]
to various types, including StyleRule
. However due to the design decisions in previous work, Selector
, which is used as a field of a variant of the StyleRule
, cannot implement IntoOwned
easily. Fundamentally, Selector
is parcel_selectors::Selector
and StyleRule
is lightningcss::StyleRule
where lightningcss
depends on parcel_selectors
.
So... I thought I needed a separate crate for this, and asked the maintainer. This time, I didn't file an issue. Instead, I just opened a PR.
One of the suggested solutions by the maintainer was creating a new crate, which is I wanted to do. So I decided to go with the path.
Steps
This was a long journey. It took much longer than initially expected, but I think it was worth it.
Add derive
, ignoring errors
- This step is done before creating a PR.
I know what I want to do. I want to make lightningcss::CssRule
, and related types implement IntoOwned
. So I just added derive
to them.
As a result, I could get a useful error message. I have to implement them for parcel_selector::Selector
and parcel_selector::SelectorList
, but I couldn't because of the reason I described above.
So I asked the maintainer and decided the way to go.
New crate, move codes
I needed to create a new package, and naming a project is the hardest thing in the world of programming. But I don't like using lots of time for naming things, so I just selected static-self
, which is quite intuitive.
I created two new crates. The first one is the actual package, and the second one is a proc-macro, which makes implementing IntoOwned
semi-automatic.
After that, I just copy-pasted codes for IntoOwned
. Both the derive macro and the runtime library. There were many red lines in IDE, but I don't care.
I cleaned up the code so I could compile them. These cleanups were about adding relevant dependencies or adding a proper mod into_owned;
statement.
Manual impl for various types
IntoOwned
is now a separate project, so it deserves the effort. I implemented IntoOwned
for Option<T>
and Box<T>
as the maintainer recommended.
Btw, since it's now a trait, I think a bunch of the special cases (e.g. Option and Box) can also be implemented with generics instead of in the derive macro so it might get a lot simpler.
impl<'any, T> IntoOwned<'any> for Option<T>
where
T: for<'aa> IntoOwned<'aa>,
{
type Owned = Option<<T as IntoOwned<'any>>::Owned>;
fn into_owned(self) -> Self::Owned {
self.map(|v| v.into_owned())
}
}
impl<'any, T> IntoOwned<'any> for Box<T>
where
T: for<'aa> IntoOwned<'aa>,
{
type Owned = Box<<T as IntoOwned<'any>>::Owned>;
fn into_owned(self) -> Self::Owned {
Box::new((*self).into_owned())
}
}
Those implementations are quite straightforward. I could remove special-case codes for Option<T>
and Box<T>
in the proc-macro.
Triage to use derive for selectors
As derive is cleaned up, I wanted to use it everywhere.
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct SelectorList<'i, Impl: SelectorImpl<'i>>(
#[cfg_attr(feature = "serde", serde(borrow))] pub SmallVec<[Selector<'i, Impl>; 1]>,
);
I added it to one of the problematic types. But if we do this, we have more problems.
Impl
is a group for actual type parameters, and it's not used as a generic type parameter of a field. However, detecting this from a procedural macro is not a good idea. So, I decided to go with manual implementations for type with Impl: SelectorImpl
.
Manual implementation for Selector
First, I tried
#[cfg(feature = "into_owned")]
impl<'any, 'i, Impl: SelectorImpl<'i>> static_self::IntoOwned<'any> for Selector<'i, Impl>
where
Impl: static_self::IntoOwned<'any>,
<Impl as static_self::IntoOwned<'any>>::Owned: SelectorImpl<'any>,
{
type Owned = Selector<'any, <Impl as static_self::IntoOwned<'any>>::Owned>;
fn into_owned(self) -> Self::Owned {
Selector(self.0, self.1.into_owned())
}
}
to know what it should look like.
Because of the bound, I needed one more implementation of IntoOwned
. I did it for Component
.
I aliased <Impl as static_self::IntoOwned<'any>>::Owned
as Sel
, for convenience.
#[cfg(feature = "into_owned")]
impl<'any, 'i, Impl: SelectorImpl<'i>, Sel> static_self::IntoOwned<'any> for Selector<'i, Impl>
where
Impl: static_self::IntoOwned<'any, Owned = Sel>,
Sel: SelectorImpl<'any>,
{
type Owned = Selector<'any, Sel>;
fn into_owned(self) -> Self::Owned {
Selector(self.0, self.1.into_owned())
}
}
Also, it became much more intuitive. This time, I also made some stupid mistakes.
Sel
is the output type, but I was confused and I tried something like
#[cfg(feature = "into_owned")]
impl<'any, 'i, Impl: SelectorImpl<'i>, Sel> static_self::IntoOwned<'any> for Selector<'i, Impl>
where
Impl: static_self::IntoOwned<'any, Owned = Sel>,
Sel: SelectorImpl<'any>,
Sel::NamespaceUrl: for<'aa> IntoOwned<'aa>, // this one
{
type Owned = Selector<'any, Sel>;
fn into_owned(self) -> Self::Owned {
Selector(self.0, self.1.into_owned())
}
}
This bound is about requesting the IntoOwned::Owned
to implement IntoOwned
, which is pretty strange.
#[cfg(feature = "into_owned")]
impl<'any, 'i, Impl: SelectorImpl<'i>, Sel> static_self::IntoOwned<'any> for Selector<'i, Impl>
where
Impl: static_self::IntoOwned<'any, Owned = Sel>,
Sel: SelectorImpl<'any>,
Impl::NamespaceUrl: for<'aa> static_self::IntoOwned<'aa, Owned = Sel::NamespaceUrl>,
Impl::NamespacePrefix: for<'aa> static_self::IntoOwned<'aa, Owned = Sel::NamespacePrefix>,
{
type Owned = Selector<'any, Sel>;
fn into_owned(self) -> Self::Owned {
Selector(self.0, self.1.into_owned())
}
}
After some time, I found that these are the correct bounds. The old NamespaceUrl
maps to the new NamespaceUrl
, and so on.
But some of the types had problems. I had to write bounds like
#[cfg(feature = "into_owned")]
impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any>
for AttrSelectorWithOptionalNamespace<'i, Impl>
where
Impl: static_self::IntoOwned<'any, Owned = NewSel>,
NewSel: SelectorImpl<'any>,
Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>,
NamespaceConstraint<(Impl::NamespacePrefix, Impl::NamespaceUrl)>:
static_self::IntoOwned<'any, Owned = NamespaceConstraint<(NewSel::NamespacePrefix, NewSel::NamespaceUrl)>>,
ParsedAttrSelectorOperation<Impl::AttrValue>:
static_self::IntoOwned<'any, Owned = ParsedAttrSelectorOperation<NewSel::AttrValue>>,
{
type Owned = AttrSelectorWithOptionalNamespace<'any, NewSel>;
fn into_owned(self) -> Self::Owned {
AttrSelectorWithOptionalNamespace {
namespace: self.namespace.into_owned(),
local_name: self.local_name.into_owned(),
local_name_lower: self.local_name_lower.into_owned(),
operation: self.operation.into_owned(),
never_matches: self.never_matches,
}
}
}
Relaxed bounds of generic implementations
I found that for<'aa> IntoOwned<'aa>
is too much for bounds of type parameters, because we only need Self<'any>
. So, I modified the bound of generic implementations first.
impl<'any, T> IntoOwned<'any> for Vec<T>
where
T: IntoOwned<'any>,
{
type Owned = Vec<<T as IntoOwned<'any>>::Owned>;
fn into_owned(self) -> Self::Owned {
self.into_iter().map(|v| v.into_owned()).collect()
}
}
So now if T
implements IntoOwned<'a>
, Vec<T>
implements IntoOwned<'a>
. Previously, additional bounds were required because the bound was T: for<'aa> x<'aa>
.
Generic impl for Box<[T]>
I found that now the problem is Box<[T]>
, because [T]
is ?Sized
and our previous implementation has *self
, which cannot be used with T: ?Sized
.
After trying to remove the Sized
bound, I found that Box<T> where T: Sized
and Box<[T]>
does not overlap. So I implemented IntoOwned
for `[
impl<'any, T> IntoOwned<'any> for Box<[T]>
where
T: IntoOwned<'any>,
{
type Owned = Box<[<T as IntoOwned<'any>>::Owned]>;
fn into_owned(self) -> Self::Owned {
self.into_vec().into_owned().into_boxed_slice()
}
}
I wrote this, and it worked!
Relax bounds derives
The procedural macro was doing
for type_param in type_param_names {
generics.make_where_clause().predicates.push_value(parse_quote! {
#type_param: 'static + for<'aa> ::static_self::IntoOwned<'aa>
})
}
but as we saw above, this is too much. I modified this to
for type_param in type_param_names {
generics.make_where_clause().predicates.push_value(parse_quote! {
#type_param: 'static + ::static_self::IntoOwned<'any>
})
}
and it worked like a charm! I thought the PR was done.
extern crate
rustc
complained about missing extern crate
, so I added one.
#[cfg(feature = "into_owned")]
extern crate static_self;
After that, I found that, still, there are many problems.
I did
#[cfg(feature = "into_owned")]
impl<'any> static_self::IntoOwned<'any> for DefaultAtRule {
type Owned = Self;
fn into_owned(self) -> Self {
self
}
}
and
#[cfg(feature = "into_owned")]
impl<'any> ::static_self::IntoOwned<'any> for Selectors {
type Owned = Self;
fn into_owned(self) -> Self::Owned {
self
}
}
but these were not enough.
Implement for tuples
For first, there were bounds like
#[cfg(feature = "into_owned")]
impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any>
for AttrSelectorWithOptionalNamespace<'i, Impl>
where
Impl: static_self::IntoOwned<'any, Owned = NewSel>,
NewSel: SelectorImpl<'any>,
Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>,
NamespaceConstraint<(Impl::NamespacePrefix, Impl::NamespaceUrl)>:
static_self::IntoOwned<'any, Owned = NamespaceConstraint<(NewSel::NamespacePrefix, NewSel::NamespaceUrl)>>,
ParsedAttrSelectorOperation<Impl::AttrValue>:
static_self::IntoOwned<'any, Owned = ParsedAttrSelectorOperation<NewSel::AttrValue>>,
{
type Owned = AttrSelectorWithOptionalNamespace<'any, NewSel>;
fn into_owned(self) -> Self::Owned {
AttrSelectorWithOptionalNamespace {
namespace: self.namespace.into_owned(),
local_name: self.local_name.into_owned(),
local_name_lower: self.local_name_lower.into_owned(),
operation: self.operation.into_owned(),
never_matches: self.never_matches,
}
}
}
I was sure that (Impl::NamespacePrefix, Impl::NamespaceUrl)
was causing at least one problem because I didn't implement IntoOwned
for tuple types. So I implemented it, and it was easy.
Use ::Owned
for generics
As I had implemented IntoOwned
for tuples, I modified the bounds to
#[cfg(feature = "into_owned")]
impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any>
for AttrSelectorWithOptionalNamespace<'i, Impl>
where
Impl: static_self::IntoOwned<'any, Owned = NewSel>,
NewSel: SelectorImpl<'any>,
Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>,
Impl::NamespacePrefix: static_self::IntoOwned<'any, Owned = NewSel::NamespacePrefix>,
Impl::NamespaceUrl: static_self::IntoOwned<'any, Owned = NewSel::NamespaceUrl>,
{
type Owned = AttrSelectorWithOptionalNamespace<'any, NewSel>;
fn into_owned(self) -> Self::Owned {
AttrSelectorWithOptionalNamespace {
namespace: self.namespace.into_owned(),
local_name: self.local_name.into_owned(),
local_name_lower: self.local_name_lower.into_owned(),
operation: self.operation.into_owned(),
never_matches: self.never_matches,
}
}
}
But there were some more problems, and I used cargo-expand
to see the generated code. The problem was that derived IntoOwned
implementation was stupid.
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum NamespaceConstraint<NamespaceUrl> {
Any,
/// Empty string for no namespace
Specific(NamespaceUrl),
}
became
impl<'any, NamespaceUrl> ::static_self::IntoOwned<'any>
for NamespaceConstraint<NamespaceUrl>
where
NamespaceUrl: 'static + ::static_self::IntoOwned<'any>,
{
type Owned = Self;
#[inline]
fn into_owned(self) -> Self {
self
}
}
meaning IntoOwned
cannot change anything about the type. The output type should be <NamespaceUrl as IntoOwned>::Owned
, so I patched the procedural macro.
Remove 'static
bound from derive
I faced another error. rustc
was complaining about 'static
lifetime, so I tried removing 'static
from the derive bound.
for type_param in type_param_names {
generics.make_where_clause().predicates.push_value(parse_quote! {
#type_param: 'static + ::static_self::IntoOwned<'any>
})
}
is modified to
for type_param in type_param_names {
generics.make_where_clause().predicates.push_value(parse_quote! {
#type_param: ::static_self::IntoOwned<'any>
})
}
and it was the answer.
for<'aa>
=> 'any
, in manual impl
I concluded that for<'aa> IntoOwned<'aa>
is way too much, so I modified all bounds in manual implementation to use 'any
instead.
Then... another problem! This time, the error message is even cryptic.
I didn't think about the associated types of SelectorImpl
. rustc
insists
Associated types of the
SelectorImpl
implementation forSelectors
do not implementIntoOwned
, soSelector<'_, Selectors>
does not implementIntoOwned
in a very cryptic language. It's right, but how can I know the cause?
Debugging & Add one assertion
There's a way to go.
#[cfg(feature = "into_owned")]
fn _assert_into_owned() {
// Ensure that we provide into_owned
use static_self::IntoOwned;
fn _assert<'any, T: IntoOwned<'any>>() {}
_assert::<SelectorList>();
}
With this assertion, rustc
shows the real problem. The problem was that types like CSSString<'i>
, which are associated types of SelectorImpl
, didn't implement IntoOwned
.
Such assertion makes many types explicit and rustc
kindly shows good error messages for explicit types.
Ah... I forgot to implement IntoOwned
for types in SelectorImpl
.
More derive
/// A pseudo class.
#[derive(Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "kind", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum PseudoClass<'i> {
// Lots of variants...
}
I just derived them.
Wrapping up
Finally, it's done! Let's see what I should highlight here.
Notable approaches
Implement at least one item manually before writing a derive macro
I wanted to use derive
for Selector
and friends, too. But I could conclude it's a waste of time before actually wasting lots of time because I implemented it to Selector
and found that it's impractical.
Analyze requirements of generics
- Note: In many cases, it's fine to postpone this.
Generic bounds work if you have bounds that are too much. But it's not ideal, and it can cause problems, just like in this case. If such an error happens, don't trust your generic implementation because "it does not error."
Making types explicit to control rustc
Typically, the types generated from type inference is useless for human, and many build tools hide them from the user. But sometimes, you want to validate the type. If so, be explicit to the compiler. If you are using a good language, the compiler will show detailed diagnostics for your explicit types.
Conclusion
I always think that it would have been better to do something and how. This is the key to changing the way you think.
P.S.
I'm sure I have a bad approach I wanted to write about, but I forgot what it was.