Boost logo

Boost :

Subject: Re: [boost] [yap] review part 3: tests + misc + summary
From: Steven Watanabe (watanabesj_at_[hidden])
Date: 2018-02-21 16:28:04


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_at_[hidden]> wrote:
> <snip>
> That looks like a great candidate for an example, so I made one out of it:
>
> https://github.com/tzlaine/yap/commit/4b383f9343a2a8affaf132c5be1eeb99a56e58df
>
> 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


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk