
AMDG On 02/20/2018 11:16 PM, Zach Laine via Boost wrote:
On Tue, Feb 20, 2018 at 9:29 AM, Steven Watanabe via Boost < boost@lists.boost.org> wrote: <snip> That looks like a great candidate for an example, so I made one out of it:
https://github.com/tzlaine/yap/commit/4b383f9343a2a8affaf132c5be1eeb99a56e58...
It took most of the examples from the documentation page you posted above, and it works for them, including nesting. For instance:
boost::yap::evaluate( let(_a = 1_p, _b = 2_p) [ // _a here is an int: 1
let(_a = 3_p) // hides the outer _a [ cout << _a << _b // prints "Hello, World" ] ], 1, " World", "Hello," );
That's verbatim from the Phoenix docs (except for the yap::evaluate() call of course), with the same behavior. The entire example is only 158 lines, including empty lines an some comment lines. The trick is to make let() a regular eager function and leave everything else lazy Yap expression stuff. I don't know if this counts as evaluation in "a single pass" as you first mentioned, but I don't care, because the user won't care either -- she can't really tell.
[snip]
evaluate(let(_a = 1_p << 3) [ _a << "1", _a << "2" ], std::cout); // prints 3132, but should print 312 Also, let(_a=_a+_a)[let(_a=_a+_a)[let(_a=_a+_a)[...]]] has exponential cost.
I don't yet get what you're suggesting here. Right now, transform()
returns whatever you tell it to,
The issue is not what I'm telling it to do, but rather what it does on its own when I don't tell it anything.
I take it from this that you mean the copying of nodes unmatched by the transform object. If so, I think this is covered by transform_strict() (as I'm provisionally calling it), that hard-errors on unmatched nodes. Does that suffice, or are there other aspects you find problematic?
except that it doesn't special-case void return. You can (I have have extensively in the examples) return an Expression or non-Expression from a transform, or from any subset of overloads your transform object provides. What is the behavior that you're suggesting?
You've chosen to make it impossible to customize the behavior of evaluate. I believe that Brook brought up evaluate_with_context, which is basically what I want.
Yes, I have. I don't believe you actually want otherwise, though. Such a tailored evaluate() would look something like this:
evaluate_with_context(expr, context, placeholder_subs...); // the subs are of course optional
What does evaluate_with_context now do? Let's say expr is "a + b". Does the context only apply to the evaluation of a and b as terminals, or does it apply to the plus operation as well?
It applies to the plus operation first. If the context has a handler for plus, then it's up to the context to handle recursion. If it does not, then it becomes evaluate_with_context(a, ctx, x...) + evaluate_with_context(b, ctx, x...)
Are such applications of the context conditional? How does the reader quickly grasp what the evaluate_with_context() call does? This seems like really muddy code to me. If you have something else in mind, please provide more detail -- I may of course be misunderstanding you.
My idea is that it would behave exactly like transform, except that the default behavior for nodes that are not handled by the context is to evaluate the operators instead of building a new expression.
<snip>
- Unwrapping terminals redux: Unwrapping terminals is convenient when you have terminals of the form: struct xxx_tag {}; auto xxx = make_terminal(xxx_tag{}); AND you are explicitly matching this terminal with auto operator()(call_tag, xxx_tag, T...) or the like. I expect that matching other terminals like double or int is somewhat more rare in real code. If you are matching any subexpression, then unwrapping terminals is usually wrong.
Why do you say that? The only way it is the way it is now is convenience, as you might have guessed. When I know I want to match a particular terminal of a particular type, I'd rather write:
auto operator(some_unary_tag, some_type && x); auto operator(some_unary_tag, some_type const & x);
versus:
auto operator(some_unary_tag, my_expr<yap::expr_kind::terminal, some_type &&> const & x); auto operator(some_unary_tag, my_expr<yap::expr_kind::terminal, some_type const &> const & x);
This latter form is what I started with, and I very quickly grew tired of writing all that.
You can always use a typedef or alias. i.e. my_term<some_type&&>.
I decided to conduct this experiment and see how it went. I removed the terminal_value() function and all its uses from default_eval.hpp; this is all that was required to disable terminal unwrapping. Almost immediately, I ran into something I did not want to deal with. From one of the tests:
decltype(auto) operator()( yap::expr_tag<yap::expr_kind::call>, tag_type, double a, double b) { /* ... */ }
Personally, I believe that this is probably a very rare situation outside of test cases and toy examples.
Now, the tag type part is not so bad. As you mentioned, I can just write my_term<tag_type> or similar. Now, what about the doubles? <snip>
If I match a generic expression that may or may not be a terminal, I do have to use yap::as_expr(), which is definitely an inconvenience:
template <typename Expr> auto operator(some_unary_tag, Expr const & x) { return some_function_of(yap::as_expr(x)); }
My intuition is that this is the most common pattern, and should be what we optimize for. Also, changing terminals to yap::expression may cause random weirdness if the transform returns an Expression. In addition, I feel that needing to call as_expr is more surprising than needing to write out the the terminal as an expression.
I agree that this is the most likely more common pattern. It's just that when you *do* want to match terminals, especially more than one at the same time, the pain of doing it without terminal unwrapping if far greater than the pain of using as_expr() in code like the common case above.
Actually... I have a much better idea. Why don't you allow transform for a non-expr to match a terminal tag transform?
I've read this a few times now and cannot parse. Could you rephrase?
struct F { auto operator()(terminal_tag, int x) { return x * 2; } }; transform(3, F{}); // returns 6 This allows you to avoid the need for as_expr. I think this behavior is consistent, when you also unwrap terminals, as you would essentially be treating terminals and the raw values as being interchangeable. In Christ, Steven Watanabe