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.