On further reflection, Andrew’s current syntax stands out, is easy to get used to after the initial shock, and — most importantly to me — leaves plenty of room for further expansion as is: you can just expand the contexts in which |# #| may be considered semantically valid, as in fact I originally thought it allowed for.
This is the way :)
I'm still not comfortable expanding that particular operator to more contexts, but we'll see how things shake out moving forward.
(Small point though: I have carefully not considered why the unquote operator %{ } is needed, and was a little confused by the various usages — why not always assume any generated string therein is unquoted when parsed? Something for Andrew to explain/you all to discuss in your meeting.)
%{x} is somewhere between an inline lambda capture and a string interpolation operator. Within a fragment (and only within a fragment) it denotes a placeholder for a value to supplied during evaluation. Specifically, it's a placeholder for whatever value x has at the point the fragment is evaluated.
consteval {
int x = 0;
-> <namespace { int n1 = %{x}; }>; // generates int n1 = 0;
++x;
-> <namespace { int n2 = %{x}; }>; // generates int n2 = 1;
}
It's entirely orthogonal to splicing. Its purpose is to turn values during constant expression evaluation into constants in injected code. It just happens to work particularly well with splicing.
Here's some code I showed in a talk that generates a sequence of initializers for a static array:
template<enumeral T>
consteval std::vector<meta::info> values_of() {
std::vector<meta::info> result;
for (meta::info e : meta::members_of(reify(T))) {
auto frag = <( {|%{e}|, %{meta::name_of(e)}} )>;
result.push_back(frag);
}
return result;
}
// elsewhere with E being an enum type:
static std::unordered_map<E, char const*> map = { |values_of<E>()|... };
Adding an operator is not the only way to do this. We can easily identify names of local variables inside of fragments and just "make it work". In fact, that's how fragments worked from 2016 until '19 or so when Wyatt and I decided to change the approach. Here's why:
- Our experience was that non-trivial fragments become really hard to read when they contain a mix of fragment-declared names, globally visible names, and names of local variables. It was also hard to figure out what was being injected. I don't remember getting negative feedback on the design, but there were definitely examples where Wyatt and I went back and forth trying to figure out what they should mean.
- Implicit "captures" make easy to shadow locals, meaning you have to be careful about the naming of declarations in your fragment---or your locals. Explicitly escaping turns out to be a really clean and obvious way of addressing that problem (as opposed to lambda-like captures). That was just weirdly limiting.
- If we can't interpolate expressions (e.g., %{meta::name_of(e)}), it forces us to declare local variables with those results and then use those names, limiting expressivity. It's probably worth noting that lambda captures allow a similar feature: [s = name_of(x)]... But fragments only ever "capture" values, so we don't need quite the same level of control over references and copies.
The other approach we considered was to explicitly declare these captures just like a lambda expression. In fact, that's how the implementation worked before 2016 and Herb suggested they should be implicit :)