2
0
mirror of https://github.com/boostorg/yap.git synced 2026-02-22 16:02:10 +00:00
Files
yap/doc/tutorial.qbk
Zach Laine d6a843f794 Doc tweaks.
2016-12-14 20:58:43 -06:00

407 lines
17 KiB
Plaintext

[section Tutorial]
[section Expressions]
_yap_ consists of expressions and functions that operate on them. Any type
that models the _Expr_ concept will work with any of the functions in _yap_
that takes an expression.
For a type `T` to model the _Expr_ concept, `T` must contain at least an
_kind_ (terminal, plus-operation, etc.) and a _tuple_ of values. That's it.
This means that user-defined templates modelling _ExprTmpl_ are very
straightforward to make.
[note The _tuple_ of values is also constrained, based on the kind of the
expression; see the full _Expr_ documentation for details.]
Here's an example:
[minimal_template]
That's a template that models _ExprTmpl_. Instantiated with the proper
template parameters, it produces _Exprs_.
Ok, so it's not that interesting by itself -- it has no operators. But we can
still use it with the _yap_ functions that take an _Expr_. Let's make a
plus-expression manually:
[minimal_template_manual_construction]
And if we evaluate it using _eval_, it does what you would expect:
[minimal_template_evaluation]
[note There is a reference model of the _ExprTmpl_ concept that comes with _yap_:
_expr_. It has all the member operators defined, and the nonmember operators
and the `if_else` pseudo-operator are defined in their repsective headers.
_expr_ is great for prototyping, and for quick-and-dirty uses of _ets_, but
its use of every single operator makes it a suboptimal choice for most use
cass.]
[note Because _yap_ operates on _Exprs_, it is possible to mix and match
_Exprs_ that are instantiations of different templates. An expression
containing an _expr_ subexpression and subexpressions instantiated from N
other _ExprTmpls_ can be passed as an argument to any of the _Expr_-accepting
_yap_ functions.]
One more thing. It is important to remember that _yap_ expressions are
all-lazy, all the time. There is no auto-evaluation of a _yap_ expression
like there was with Proto. If you want your expressions to be evaluated, you
must call _eval_ or _eval_as_, enable _conv_op_m_ (a dubious solution), or
define non-lazy operations that force evaluation where and when you want it.
This last approach is usually the right one, and there are lots of examples of
how to do this in the Examples section.
[endsect]
[section Kinds of Expressions]
There are four special kinds of expression.
[variablelist Special Expression Kinds
[[_terminal_] [A terminal contains a non-Expression value, and represents the leaf-node in an
expression tree.]]
[[_placeholder_] [A placeholder is a terminal that acts as a stand-in for some value to be provided later.]]
[[_if_else_] [An `if_else` expression is analogous to the C++ ternary operator (`?:`), but
without the common-type conversion logic.]]
[[_expr_ref_] [An `expr_ref` expression is one that acts as a (possibly `const`) lvalue reference to another expression. It exists to prevent unnecessary copies of expressions.]]
]
The other expression kinds are all the overloadable operators, See _kind_ for
the full list.
[endsect]
[section Operators]
Let's see an expression template type with some operators:
[lazy_vector_decl]
Those macros are used to define operator overloads that return _Exprs_. As
shown here, that sort of operator can be mixed with normal, non-lazy ones.
Use of the macros is not necessary (you can write your own operators that
return _Exprs_ if you like), but it is suitable 99% of the time.
Making the operators easy to define like this allows you to define custom
expression templates that have only the operators defined that are appropriate
for your use case.
[endsect]
[section Operator Macros]
There are macros that define binary operators as members and macros that
define them as nonmembers. For a given operator, the member-defining macro
will define an operator that accepts anything on the right (making a terminal
out of a non-_Expr_ operand); the nonmember-defining macro will define an
operator that will only accept a non-_Expr_ on the left and an _Expr_ on the
right. This is to avoid creating ambiguous overload sets. Here are the
macros and their uses:
[table Unary and Binary Operator-Defining Macros
[[Macro] [Use] [First/Left Operand Type] [Right Operand Type] [Notes]]
[[_unary_member_m_] [Unary member operators.] [Any type.] [--] []]
[[_binary_member_m_] [Binary member operators.] [Any type.] [Any type.] []]
[[_binary_free_m_] [Binary free operators.] [Any non-_Expr_.] [Any _Expr_.] []]
[[_udt_unary_m_] [Free operators defined over types constrained by a type trait (e.g. all `std::map<>`s).] [Any non-_Expr_ that satisfies the given type trait.] [--] []]
[[_udt_udt_binary_m_] [Free operators defined over types constrained by a pair of type traits (e.g. a `std::map<>` on the left, and a `std::vector<>` on the right). Useful for type-asymmetric operators.] [Any non-_Expr_ that satisfies the left-hand type trait.] [Any non-_Expr_ that satisfies the right-hand type trait.] []]
[[_udt_any_binary_m_] [Free operators defined over pairs of types, one constrained by a type trait and one not (e.g. a `std::list<>` on either side, and anything on the other).] [Any non-_Expr_.] [--] [At least one parameter must satisfy the given type trait.]]
]
[table The Other Operator-Defining Macros
[[Macro] [Use] [Operands] [Notes]]
[[_member_call_m_] [Member call operator.] [Any type.] []]
[[_expr_if_else_m_] [Free `if_else()` function that requires at least one parameter to be an expression.] [Any type.] [At least one parameter must be an _Expr_.]]
[[_udt_any_if_else_m_] [Free `if_else()` function that requires at least one parameter to satisfy the given type trait.] [Any non-_Expr_.] [At least one parameter must satisfy the given type trait.]]
]
[note Operands are handled in a uniform way across all functions defined by
all the macros listed here. See _how_treated_ for details.]
[endsect]
[section How Expression Operands Are Treated]
For any _expr_ operator overload, or any function defined using one of the
function definition macros, operands are treated in a uniform way.
The guiding design principle here is that an expression built using _yap_
should match the semantics of a builtin C++ expression as closely as possible.
This implies that an rvalue remain treatable as a temporary (as it may in fact
be) throughout the building and transformation of an expression, and that an
lvalue should retain its connection to the underlying named entity to which it
refers.
For example, if you see
auto expr = a + 1;
you should expect that `a` will be an lvalue reference to some object of type
`decltype(a)`, regardless of whether `a` is an _Expr_ or a builtin type.
Similarly, you should expect the `1` to be an rvalue, whether wrapped in a
terminal or not.
Let's take a quick look at _make_term_. If you call it with a `T` rvalue, the
terminal's value type is a `T`, and the rvalue gets moved into it. If you
call it with a `T [const]` lvalue, the value type is `T [const] &`, and the
reference refers to the lvalue (read `[const]` as "possibly
`const`-qualified"). This is important because you might write through the
terminal later in an assignment operation. You don't want to lose the ability
to do this, or be forced to write some Baroque pile of code to do so -- it
should be natural and easy.
And it is:
[assign_through_terminal]
Now, there is a wrinkle. _yap_'s lazy expressions can be built piecemeal:
auto subexpr = boost::yap::make_terminal(1) + 2;
// This is fine, and acts more-or-less as if you wrote "1 / (1 + 2)".
auto expr = 1 / subexpr;
whereas C++'s eager builtin expressions cannot:
auto subexpr = 1 + 2; // Same as "int subexpr = 3;". Hm.
auto expr = 1 / subexpr; // Same as "int expr = 0;" Arg.
Ok, so since you can build these lazy _yap_ expressions up from
subexpressions, how do we treat the subexpressions? We treat them in exactly
the same way as _make_term_ treats its operands. Rvalues are moved in, and
lvalues are captured by (possibly `const`) reference.
[note If you want to subvert the capture-by-reference semantics of using
subexpressions, just `std::move()` them. That will force a move -- or copy of
values for which move is not defined.]
The capture-by-reference behavior is implemented via a special _kind_,
_expr_ref_. An `expr_ref` expression has a single data element: a (possibly
`const` (Can I stop saying that every time? You get it, right? Ok, good.))
reference to an expression. This additional level of indirection causes some
complications at times, as you can see in the examples. Fortunately, the
complications are not overly cumbersome.
So, given the rules so far, here is a comprehensive breakdown of what happens
when an operand is passed to a _yap_ operator. In this table, `expr` is an
_ExprTmpl_, and `T` is a non-_Expr_ type. `E` refers to any non-`expr_ref`
_Expr_. _yap_ does a partial decay on non-_Expr_ operands, in which `cv` and
reference qualifiers are left unchanged, but arrays are decayed to pointers
and functions are decayed to function pointers. `PARTIAL_DECAY(T)` indicates
such a partial decay of `T`.
[table Operand Handling
[[Operand] [Captured As] [Notes]]
[[`T const &`] [`expr<expr_kind::terminal, boost::hana::tuple<PARTIAL_DECAY(T) const &>>`] []]
[[`T &`] [`expr<expr_kind::terminal, boost::hana::tuple<PARTIAL_DECAY(T) &>>`] []]
[[`T &&`] [`expr<expr_kind::terminal, boost::hana::tuple<PARTIAL_DECAY(T)>>`] [Operand moved.]]
[[`E const &`] [`expr<expr_kind::expr_ref, boost::hana::tuple<E const &>>`] []]
[[`E &`] [`expr<expr_kind::expr_ref, boost::hana::tuple<E &>>`] []]
[[`E &&`] [`E`] [Operand moved.]]
[[`expr<expr_kind::expr_ref, ...> const &`] [`expr<expr_kind::expr_ref, ...>`] []]
[[`expr<expr_kind::expr_ref, ...> &`] [`expr<expr_kind::expr_ref, ...>`] []]
[[`expr<expr_kind::expr_ref, ...> &&`] [`expr<expr_kind::expr_ref, ...>`] [Operand moved.]]
]
The partial decay of non-_Expr_ operands is another example of how _yap_
attempts to create expression trees that are as semantically close to builtin
expressions as possible.
[endsect]
[section Customization Points]
There are a number of _cps_ defined by _yap_ that are used during expression
evaluation. _yap_ _cps_ are patterned after the ones in the _range_v3_
library, the basis for the ISO Ranges TS. To override one, you create a
function with the same name in your own namespace; it gets picked up via ADL.
By creating your own version of one of these _cps_, you can change how
expressions are evaluated in _eval_ and _eval_as_.
For example: first we define a UDT `number` and an override of _yap_'s
`eval_plus` for it,
[eval_plus_example_decls]
then we use it to evaluate expressions containing `number`s, even though there
is no user-defined plus operator for `number`:
[eval_plus_example_use]
There is a _cp_ for each operator (including `if_else`), one for doing
implicit transforms during evaluation, called `transform_expression()`, and
yet one more for replacing the entire implementation of _eval_as_, called
`eval_expression_as()`. The complete list of the _cps_ can be found in the
`function_objects` namespace in the _ops_header_.
You just saw an example of an operator _cp_ above. Here's an example
overriding the `transform_expression()` _cp_:
[transform_expression_example_decls]
and using it:
[transform_expression_example_use]
The `eval_expression_as()` _cp_ works similarly.
You can create as many overloads of a _cp_ `C` as you like. If there are 100
overloads of `C`, any of them that match subexpressions within the expression
being evaluated will be used; you only need to make sure that the overloads
are mutually unambiguous.
This is especially important when it comes to the `transform_expression()`
_cp_. Providing multiple overloads can be very useful for defining an
implicit set of transformations that you always want applied to all your
expression evaluations. For example, if you define a template overload and a
nontemplate overload for a certain _cp_ `C`, the template will only apply when
the nontemplate does not (due to the normal C++ overload resolution rules);
this allows general-casing in the template overload, and special-casing in the
nontemplate overload.
[heading How the transform_expression Customization Point Is Applied]
If at any point during the execution of _eval_ or _eval_as_, if `expr` is the
current subexpression and `transform_expression(expr)` is well-formed,
`transform_expression(expr)` gets called in place of the normal evaluation of
`expr`.
[important Subexpressions of `expr` are not evaluated recursively, and in fact
they are not evaluated at all! The arguments passed to
`transform_expression()` are _Exprs_, not evaluated values.]
If you want recursive evaluation within your `transform_expression()`
override, just use `evaluate()` directly:
This transform takes expressions of the form `a * x + y` and calls a function
that does that computation in one step:
[naxpy_transform_decl]
and since `evaluate()` is called on each operand in the transform, the
transform is applied to each parenthesized subexpression here, and then to the
top-level expression:
[naxpy_transform_use]
[heading How the eval_expression_as Customization Point Is Applied]
The `eval_expression_as()` _cp_ is only applied at the top level, replacing a
call to _yap_'s version. Like `transform_expression()`, it subverts the
normal recursive evaluation of the expression being evaluated. Also like
`transform_expression()`, it is passed an _Expr_.
`eval_expression_as()` may be useful if you want to evaluate an expression in
a very different way depending on what the desired result type is.
[note Evaluation of _expr_'s optional conversion operator template is done via
`eval_expression_as()`. See _conv_op_m_ for details.]
[heading How The Other Customization Points Are Applied]
Each of _eval_ and _eval_as_ evaluates its given expression recursively, depth
first. For any given subexpression `E` of kind `K` with subexpressions `S0`
... `Sn`, `E` is evaluated as:
eval_K(evaluate(S0), ... evaluate(Sn))
For example, using `expr_kind::plus`:
eval_plus(evaluate(left(e)), evaluate(right(e)))
This means that a (sufficiently general) templated `eval_plus()` _cp_ override
would apply to all plus operations in an expression tree, even nested ones.
All the _cps_ except `transform_expression()` and `eval_expression_as()`
receive evaluated values in their arguments, not _Exprs_. That is, unless
some of the evaluated values happen to be _Exprs_ -- you may have written an
expression that evaluates to an expression. You may also be a weirdo.
[endsect]
[section Transform Matching]
In _yap_ a _XForm_ is a _Callable_ that has *zero or more* overloads that
model the _ExprXForm_ or _TagXForm_ concepts.
An _ExprXForm_ overload takes the expression to be transformed:
[expr_xform]
_ExprXForms_ are most useful when you want to transform a narrow set of
expression types (perhaps only one). In particular, you can distinguish
between `const` and non-`const`, reference and non-reference, etc., in the
expression and its operands in a way that you have less control over with the
other kind of transform.
A _TagXForm_ overload takes a tag that indicates the _kind_ of the expression
to be transformed, and then (loosely) the value of each operand of the
epxression to be transformed:
[tag_xform]
_TagXForms_ are most useful when the transform needs to match an expression
without regard to whether its operands are _expr_ref_ expressions, or -- if
they are terminals -- whether they contain or refer to their values.
_TagXForms_ tend to be far more concise.
[heading A More Rigorous Description of TagTransform Parameters]
That "(loosely)" before probably bothered you, right? Me too. Each non-tag
parameter is passed to a _TagXForm_ by calling an operand accessor appropriate
to `expr`'s kind, and then calling _value_ on the result. For example,
consider a plus expression `expr`. The _TagXForm_ on a transform object
`xform` would be called like this:
xform(plus_tag, value(left(expr)), value(right(expr)))
The operand accessors (_left_ and _right_ in this example) all dereference
_expr_ref_ expressions before operating on them, and _value_ does the same.
When given a terminal, _value_ also returns the value of the terminal.
The result of this pattern is that you can effectively ignore the presence of
_expr_ref_ expressions when writing a _TagXForm_. You can also just deal with
the values inside terminals, and not the terminals themselves. Also, you can
match all terminal value qualifiers (`const` or not, lvalue or rvalue)
uniformly with a `T const &` parameter. Finally, you can write _TagXForm_
parameter types that can catch conversions; for instance, you can match any
negation expression containing a terminal, *or a reference to one*, containing
a value convertible to `double` like this:
struct xform
{
auto operator() (boost::yap::negate_tag, double x)
{ return /* ... */; }
}
That will match a negation of a terminal containing an `unsigned int`,
`unsigned int &`, `int const &`, `float &&`, etc. It will also match a
negation of a reference to such a terminal.
[heading Mixing the Two Kinds of Transforms]
You can have two overloads in your transform that match an expression, one an
_ExprXForm_ and one a _TagXForm_, and there will not be any ambiguity. The
_TagXForm_ is prefered, except in the case of a call expression, in which case
the _ExprXForm_ is preferred. I know, I know. You don't have to worry about
amiguity, but save youself some confusion and mix the two kinds of overloads
as little as possible.
[endsect]
[endsect]