-
-
Notifications
You must be signed in to change notification settings - Fork 109
computation in interpolation #30
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
computation in interpolation #30
Conversation
implement #10
|
A disadvantage of this approach is this: If support for To keep forward compatibility, we could disallow computation inside of loops altogether. The following example compiles with the current PR, but would not compile if let foo = ["1", "2"];
let iter = foo.iter();
let tokens = quote!(#( #iter, #{"+"}, )*);
let expected = r#""1" , "+" , "2" , "+" ,"#;
assert_eq!(expected, tokens.as_str()); |
|
The current design has one advantage, say I added this documentation
Let's assume for one moment, that we adopt the alternatively and scan for
Disadvantages:
IMO these are a lot of disadvantages only to support iterators If you look at the code, the implementation of the current proposal is pretty straight forward. @dtolnay how do you currently feel about this approach? |
|
PS: To wrap things up I think we have three alternatives
When I'd ignore the limitations of the current macro system for one moment, I'd rate them |
As far as I can tell, this approach does not simplify the use case linked to at the top of #10. Is that correct? let mut field_names = fields.iter().map(|f| &f.name);
let mut field_idents = fields.iter().map(|f| &f.ident);
quote! {
for field in fields {
match field.name() {
#(
#field_names => /* ... */,
)*
_ => unreachable!(),
}
}
::std::result::Result::Ok(#ident {
#(
#field_idents: /* ... */,
)*
})
} |
|
My experience has been that code that could be written using computation inside of interpolation is almost always clearer factored a different way. The single exception to this is computations of the form |
Correct, with With my 'secret favourite' quote! {
for field in fields {
match field.name() {
#(
#{ fields.iter().map(|f| &f.name) } => /* ... */,
)*
_ => unreachable!(),
}
}
::std::result::Result::Ok(#ident {
#(
#{ fields.iter().map(|f| &f.ident) } : /* ... */,
)*
})
}I feel that's intuitive since the compiler will give hints, that it expected an iterator if That would really be the best option IMO, but With let iter1 = fields.iter();
let iter2 = fields.iter();
quote! {
for field in fields {
match field.name() {
#(
#{ #iter1.name } => /* ... */,
)*
_ => unreachable!(),
}
}
::std::result::Result::Ok(#ident {
#(
#{ #iter2.ident } : /* ... */,
)*
})
} |
Sorry, I am completely lost here. Lol. :-)
|
|
PS: My use case is mostly the following: struct Foo { /* some fields elided */ }
impl ToTokens for Foo {
fn to_tokens(&self, tokens: &mut Tokens) {
tokens.append(quote!(
/* ... */ #{self.lorem}
/* ... */ #{self.ipsum}
/* ... etc. ... */
))
}
}I think it's a significant pain that we can't use I think the customized iterator use case is not as important as struct/array access. Because in the iterator use case, there is a significant benefit in readability to pull that logic out of |
Sure, here is an example from real Serde code. It assigns a bunch of variables, then collects them into a struct or tuple result. Any place I see let result = if is_struct {
let names = fields.iter().map(|f| &f.ident);
quote! {
#type_path { #( #names: #vars ),* }
}
} else {
quote! {
#type_path ( #(#vars),* )
}
};
quote! {
/* assign some variables */
_serde::export::Ok(#result)
}Compare this to a hypothetical use of this PR where the generated code and generating code are jumbled together. I would not want to encourage this style because to me it seems optimized for the writer and not for the reader, which is not how things should be. quote! {
/* assign some variables */
_serde::export::Ok(#{
if is_struct {
let names = fields.iter().map(|f| &f.ident);
quote! {
#type_path { #( #names: #vars ),* }
}
} else {
quote! {
#type_path ( #(#vars),* )
}
}
})
} |
It represents field access on nested structs, for example |
The postgres-derive code would be simplified to: quote! {
for field in fields {
match field.name() {
#(
#{fields.name} => /* ... */,
)*
_ => unreachable!(),
}
}
::std::result::Result::Ok(#ident {
#(
#{fields.ident}: /* ... */,
)*
})
} |
|
Ok, thanks - now I get it. :-) Regarding overly confusing nested stuff: Is it a hard constraint for you, to block this, or is it just a nice-to-have to be a bit more restrictive? Regarding We could of course special-case In comparison quote! {/* ... */
$(
#{#fields.ident}: /* ... */,
)*
/* ... */}Note: In your last comment you would iterate twice over
quote! {/* ... */
#(
#{ fields.iter().map(|f| &f.ident) }: /* ... */,
)*
/* ... */}It would expand to something like this: {
let mut _s = $crate::Tokens::new();
for _x in { fields.iter().map(|f| &f.ident) } {
$crate::ToTokens::to_tokens(&_x, &mut _s);
_s.append(":");
_s.append(/* ... */);
_s.append(",");
}
_s
}Note the introduction of a local identifier |
|
PS: Sorry for my super long comments. ^^ |
|
Ok, I'm starting to make mistakes - I'll stop after this comment for now. I just re-discovered the problem which I think is blocking Say we have multiple computations in a repetition like quote! {/* ... */
#(
#{ fields1.iter().map(|f| &f.ident) }: #{ fields2.iter().map(|f| &f.ident) },
)*
/* ... */}It would expand to something like this: {
let mut _s = $crate::Tokens::new();
for (_x, _y) in { fields1.iter().map(|f| &f.ident) }.into_iter().zip({ fields2.iter().map(|f| &f.ident) }) {
$crate::ToTokens::to_tokens(&_x, &mut _s);
_s.append(":");
$crate::ToTokens::to_tokens(&_y, &mut _s);
_s.append(",");
}
_s
}The headache is, how would we get distinct identifiers |
|
Thanks for all your work on this and for exploring the design space so thoroughly. At this point I think that while some of the options improve readability in some cases, on balance this feature would be detrimental to readability and maintainability of libraries using |
|
FWIW the reasoning behind my preference against a sandboxed approach was based on two concerns
When I was giving complex examples for the non sandboxed version I was trying to argue that it doesn't have these problems. In fact the implementation is really straight forward and it's API surface can be explained well in a terms of existing concepts. It was never my intention to say that complex computations in interpolation would be desirable. Also I disagree that computations in repetitions are particularly important. IMO support for the pattern I used complex examples but I wouldn't advocate anything beyond |
|
Instead of sandboxing it would be easier to implement a heuristic validation pass on |
|
The error message for trying to use e.g. a closure inside a computation would then be something like unexpected token |
|
We wouldn't even need to whitelist |
|
I think I am onto something - I will update this PR later. :-) |
implement #10
The design principle is very simple, e.g.
quote!(A #{ B } C)will expand toHere
{ B }can be any valid Rust code block as long as its result implementsToTokens.This simple implementation just ignores all quote-variables
#xinB. Therefore#{ B }will just behave like a black box inside of e.g. a loop.This black-box behaviour theoretically allows to nest a
quote!(#x)or other macros inside of interpolation blocks and allows for "local reasoning" about the meaning of some tokens. Whether deep nesting would be good practice is on a different page.To answer the design questions
quote! { #( #{...} )* }will therefore evaluate to zero tokens, because the loop does not find any iterator. If#{...}evaluates to an iterator you still need to bind the result to a variable first and loop over that. Sorry. (*)(*) In theory we could try to interpret
#{...}in loops as iterators, as we do with any#foo. But we will lack a proper identifier for#{...}, since the current macro_rules system does not allow generation of identifiers like__my_local_iterator_005. We could implement a helper macro with a global state (yes that is possible via higher order macros) likepop_iterator_ident(). The problem with that is, that it will only support a finite amount of calls until the supply is exhausted. I'd say we circumvent that trouble and don't support this pattern until Rust support generation of identifiers in macros.